Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ad5a635
fix(expense): prevent IOU avatar from combining when sending manual a…
marufsharifi Apr 15, 2026
d706e24
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi Apr 16, 2026
1dd2588
fix: use modifiedAmount after cache clear
marufsharifi Apr 16, 2026
5b168b7
fix: ignore empty-message deleted IOU actions
marufsharifi Apr 16, 2026
141b874
fix: handle deleted IOU actions safely
marufsharifi Apr 16, 2026
c9ed1ab
test: cover cache-clear sender fallback
marufsharifi Apr 16, 2026
dbc5e98
test: fix spelling in report preview test
marufsharifi Apr 16, 2026
306729f
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi Apr 21, 2026
99f96c5
fix: handle partial IOU actions after cache clear
marufsharifi Apr 21, 2026
cfc43d2
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi Apr 22, 2026
c5d45a8
fix: ignore in-progress scan placeholders
marufsharifi Apr 22, 2026
f2f1465
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi Apr 24, 2026
9af8483
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi Apr 27, 2026
97fa714
fix: handle legacy deleted IOU actions in report preview sender
marufsharifi Apr 27, 2026
c96f90e
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi May 1, 2026
da5ff2a
fix: handle stale preview counts after load
marufsharifi May 1, 2026
e598e53
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi May 5, 2026
2522c9f
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi May 7, 2026
c897d0f
fix: improve report preview sender
marufsharifi May 7, 2026
5e87640
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi May 11, 2026
cf0a7c4
fix: improve report preview sender rehydration
marufsharifi May 11, 2026
6bfc2ab
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi May 12, 2026
f11d00b
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi May 12, 2026
3f8d962
test: fix report action avatar fixture
marufsharifi May 12, 2026
926ab7b
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi May 14, 2026
0ee0bb0
fix: handle pending scan preview sender
marufsharifi May 14, 2026
3a9837c
fix: improve preview sender fallback during scan
marufsharifi May 14, 2026
9a3d762
fix: polish preview sender
marufsharifi May 14, 2026
246db86
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi May 16, 2026
ee5287b
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi May 19, 2026
05cca00
fix: improve report preview sender inference
marufsharifi May 19, 2026
c52150d
fix: polish review feedback
marufsharifi May 19, 2026
d25d9e7
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi May 20, 2026
b239a7a
fix: improve deleted IOU action filtering
marufsharifi May 20, 2026
ba06bc9
Merge branch 'main' into fix/iou-avatar-merged-on-manual-scan-expense
marufsharifi May 21, 2026
66131c2
fix: prefer modifiedAmount for preview direction
marufsharifi May 21, 2026
921fb48
test: clean up review feedback
marufsharifi May 21, 2026
c75f048
fix: make derived report preview avatars reactive
marufsharifi May 21, 2026
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
17 changes: 8 additions & 9 deletions src/components/ReportActionAvatars/useReportActionAvatars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import usePolicy from '@hooks/usePolicy';
import useReportIsArchived from '@hooks/useReportIsArchived';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {addSMSDomainIfPhoneNumber} from '@libs/PhoneNumber';
import {
getDelegateAccountIDFromReportAction,
getHumanAgentAccountIDFromReportAction,
getHumanAgentFirstName,
getOriginalMessage,
getReportAction,
getReportActionActorAccountID,
isMoneyRequestAction,
} from '@libs/ReportActionsUtils';
Expand All @@ -31,6 +31,7 @@ import {
import {getDefaultAvatar} from '@libs/UserAvatarUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {getReportActionByIDSelector} from '@src/selectors/ReportAction';
import type {InvitedEmailsToAccountIDs, OnyxInputOrEntry, Policy, Report, ReportAction} from '@src/types/onyx';
import type {Icon as IconType} from '@src/types/onyx/OnyxCommon';
import useReportPreviewSenderID from './useReportPreviewSenderID';
Expand Down Expand Up @@ -77,15 +78,13 @@ function useReportActionAvatars({
const chatReport = isReportAChatReport ? report : reportChatReport;
const iouReport = isReportAChatReport ? undefined : report;

let action;
const derivedActionReportID = iouReport?.parentReportActionID ? (chatReport?.reportID ?? iouReport?.chatReportID) : reportChatReport?.reportID;
const derivedActionID = iouReport?.parentReportActionID ?? (!iouReport ? chatReport?.parentReportActionID : undefined);
const [derivedAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(!passedAction ? derivedActionReportID : undefined)}`, {
selector: (actions) => getReportActionByIDSelector(actions, derivedActionID),
});

if (passedAction) {
action = passedAction;
} else if (iouReport?.parentReportActionID) {
action = getReportAction(chatReport?.reportID ?? iouReport?.chatReportID, iouReport?.parentReportActionID);
} else if (!!reportChatReport && !!chatReport?.parentReportActionID && !iouReport) {
action = getReportAction(reportChatReport?.reportID, chatReport.parentReportActionID);
}
const action = passedAction ?? derivedAction;

const [actionChildReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${action?.childReportID}`);

Expand Down
216 changes: 203 additions & 13 deletions src/components/ReportActionAvatars/useReportPreviewSenderID.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import {convertAttendeesToArray} from '@libs/AttendeeUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils';
import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
import {getOriginalMessage, isMoneyRequestAction, isSentMoneyReportAction} from '@libs/ReportActionsUtils';
import {getIOUActionForTransactionID, getOriginalMessage, isDeletedParentAction, isMoneyRequestAction, isSentMoneyReportAction} from '@libs/ReportActionsUtils';
import {isDM, isIOUReport} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx';
import {hasOnceLoadedReportActionsSelector, isLoadingInitialReportActionsSelector} from '@src/selectors/ReportMetaData';
import type {OriginalMessageIOU, Policy, Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx';

function getSplitAuthor(transaction: Transaction, splits?: Array<ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU>>) {
const {originalTransactionID, source} = transaction.comment ?? {};
Expand Down Expand Up @@ -39,6 +40,94 @@ const getSplitsSelector = (actions: OnyxEntry<ReportActions>): Array<ReportActio
.filter((act) => getOriginalMessage(act)?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT);
};

function getTransactionDirectionSign(transaction: Transaction): number | undefined {
const modifiedAmount = Number(transaction.modifiedAmount);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should modifiedAmount take precedence over amount?


if (Number.isFinite(modifiedAmount) && modifiedAmount !== 0) {
return Math.sign(modifiedAmount);
}

if (transaction.amount !== 0) {
return Math.sign(transaction.amount);
}

return undefined;
}

function hasPendingScanStateAndUnknownDirection(transaction: Transaction): boolean {
if (transaction.receipt?.source === undefined) {
return false;
}

if (transaction.receipt?.state !== CONST.IOU.RECEIPT_STATE.SCAN_READY && transaction.receipt?.state !== CONST.IOU.RECEIPT_STATE.SCANNING) {
return false;
}

if (getTransactionDirectionSign(transaction) !== undefined) {
return false;
}

return true;
}

function getPendingScanActorAccountID(transaction: Transaction, action: OnyxEntry<ReportAction>, iouReport: OnyxEntry<Report>, chatReport: OnyxEntry<Report>): number | undefined {
if (!hasPendingScanStateAndUnknownDirection(transaction)) {
return undefined;
}

const chatLastActorAccountID = Number(chatReport?.lastActorAccountID);
const validPendingScanActorAccountIDs = new Set([action?.childOwnerAccountID, action?.childManagerAccountID, iouReport?.ownerAccountID, iouReport?.managerID].filter(Boolean));

if (
Number.isFinite(chatLastActorAccountID) &&
chatLastActorAccountID > 0 &&
chatLastActorAccountID !== CONST.ACCOUNT_ID.CONCIERGE &&
validPendingScanActorAccountIDs.has(chatLastActorAccountID)
) {
return chatLastActorAccountID;
}

const previewLastActorAccountID = Number(action?.childLastActorAccountID);

if (!Number.isFinite(previewLastActorAccountID) || previewLastActorAccountID <= 0) {
return undefined;
}

return previewLastActorAccountID;
}

function getAccountIDFromTransactionDirection(transaction: Transaction, action: OnyxEntry<ReportAction>, iouReport: OnyxEntry<Report>): number | undefined {
const directionSign = getTransactionDirectionSign(transaction);

if (directionSign === undefined) {
return undefined;
}

const accountID = Number(directionSign > 0 ? (action?.childOwnerAccountID ?? iouReport?.ownerAccountID) : (action?.childManagerAccountID ?? iouReport?.managerID));

if (!Number.isFinite(accountID) || accountID <= 0) {
return undefined;
}

return accountID;
}

function isExplicitlyDeletedIOUAction(iouAction: ReportAction): boolean {
const originalMessage = getOriginalMessage(iouAction) as OriginalMessageIOU | undefined;

if (originalMessage?.deleted || isDeletedParentAction(iouAction) || iouAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
return true;
}

const message = iouAction.message;

if (Array.isArray(message)) {
return message.some((fragment) => !!fragment?.deleted);
}
Comment on lines +124 to +126

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat empty-message IOU actions as deleted

isExplicitlyDeletedIOUAction() does not mark IOU actions with message: [] as deleted, even though this legacy deleted shape is handled elsewhere via isDeletedAction(). In mixed old/new data, those deleted actions can stay in activeIOUActions, which then skews sender inference (e.g., extra actor IDs affecting hasCompleteActionCoverage / single-sender checks) and can produce the wrong avatar state for report previews after hydration. Add a message.length === 0 check (or reuse isDeletedAction) so legacy deleted IOU actions are consistently excluded.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the deleted-action handling so IOU actions with message: [] are treated as deleted.


return !!message?.deleted;
}

type GetReportPreviewSenderIDParams = {
iouReport: OnyxEntry<Report>;
action: OnyxEntry<ReportAction>;
Expand All @@ -48,25 +137,107 @@ type GetReportPreviewSenderIDParams = {
splits: Array<ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU>> | undefined;
policy: OnyxEntry<Policy>;
currentUserAccountID: number;
hasFinishedInitialReportActionsLoad?: boolean;
};

function getReportPreviewSenderID({iouReport, action, chatReport, iouActions, transactions, splits, policy, currentUserAccountID}: GetReportPreviewSenderIDParams): number | undefined {
function getReportPreviewSenderID({
iouReport,
action,
chatReport,
iouActions,
transactions,
splits,
policy,
currentUserAccountID,
hasFinishedInitialReportActionsLoad,
}: GetReportPreviewSenderIDParams): number | undefined {
const isOptimisticReportPreview = action?.isOptimisticAction && action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && isIOUReport(iouReport);

if (isOptimisticReportPreview) {
return currentUserAccountID;
}
const loadedTransactionCount = transactions?.length ?? 0;
const childMoneyRequestCount = action?.childMoneyRequestCount ?? 0;
const activeMoneyRequestCount = iouReport?.transactionCount ?? childMoneyRequestCount;
const activeIOUActions = iouActions?.filter((iouAction) => !isExplicitlyDeletedIOUAction(iouAction)) ?? [];
const uniqueIOUActionActorMap = new Map<string, number>();

// 1. If all amounts have the same sign - either all amounts are positive or all amounts are negative.
// We have to do it this way because there can be a case when actions are not available
// See: https://github.com/Expensify/App/pull/64802#issuecomment-3008944401
for (const iouAction of activeIOUActions) {
const iouTransactionID = (getOriginalMessage(iouAction) as OriginalMessageIOU | undefined)?.IOUTransactionID;

const areAmountsSignsTheSame = new Set(transactions?.map((tr) => Math.sign(tr.amount))).size < 2;
if (!iouTransactionID || iouAction.actorAccountID === undefined) {
continue;
}

uniqueIOUActionActorMap.set(iouTransactionID, iouAction.actorAccountID);
}

if (!areAmountsSignsTheSame) {
const hasCompleteActionCoverage = activeMoneyRequestCount > 0 && uniqueIOUActionActorMap.size >= activeMoneyRequestCount;
const areAllActiveChildRequestsCreatedByOneActor = uniqueIOUActionActorMap.size > 0 && new Set(uniqueIOUActionActorMap.values()).size < 2;
const canInferFromIOUActionsDuringPartialHydration = loadedTransactionCount > 0 && hasCompleteActionCoverage && activeIOUActions.length > 0 && areAllActiveChildRequestsCreatedByOneActor;

// After refresh, the preview action can hydrate before all active child transactions.
// Avoid collapsing to one avatar unless the available IOU actions already prove the remaining
// active requests all belong to the same sender.
if (!hasFinishedInitialReportActionsLoad && activeMoneyRequestCount > loadedTransactionCount && !canInferFromIOUActionsDuringPartialHydration) {
return undefined;
}

const transactionActorAccountIDs = transactions?.map((transaction) => {
return getIOUActionForTransactionID(activeIOUActions, transaction.transactionID)?.actorAccountID ?? getPendingScanActorAccountID(transaction, action, iouReport, chatReport);
});
const transactionSigns = transactions?.map((transaction) => getTransactionDirectionSign(transaction)) ?? [];

const hasActorAccountIDForEachTransaction =
activeIOUActions.length > 0 && !!transactionActorAccountIDs && transactionActorAccountIDs.length > 0 && transactionActorAccountIDs.every((accountID) => accountID !== undefined);

// 1. Use actorAccountID when it is available for every transaction. Otherwise, fall back to known transaction direction only.
if (hasActorAccountIDForEachTransaction) {
const areAllTransactionsCreatedByOneActor = new Set(transactionActorAccountIDs).size < 2;

if (!areAllTransactionsCreatedByOneActor) {
return undefined;
}
} else {
const transactionsWithUnknownDirection = (transactions ?? []).filter((transaction, index) => transactionSigns.at(index) === undefined);
const hasUnknownDirection = transactionSigns.some((sign) => sign === undefined);
const unknownDirectionComesOnlyFromPendingScans = transactionsWithUnknownDirection.length > 0 && transactionsWithUnknownDirection.every(hasPendingScanStateAndUnknownDirection);
const hasOnlyUnknownDirections = transactionSigns.length > 0 && transactionSigns.every((sign) => sign === undefined);
const hasOnlyUnknownNonPendingScanDirections =
hasOnlyUnknownDirections && transactionsWithUnknownDirection.every((transaction) => !hasPendingScanStateAndUnknownDirection(transaction));

if (unknownDirectionComesOnlyFromPendingScans) {
const inferredActorAccountIDs = (transactions ?? [])
.map((transaction, index) => {
return transactionActorAccountIDs?.at(index) ?? getAccountIDFromTransactionDirection(transaction, action, iouReport);
})
.filter((accountID): accountID is number => accountID !== undefined);

if (new Set(inferredActorAccountIDs).size > 1) {
return undefined;
}
}

if (hasUnknownDirection && !unknownDirectionComesOnlyFromPendingScans && !hasOnlyUnknownNonPendingScanDirections) {
return undefined;
}

const knownTransactionSigns = transactionSigns.filter((sign): sign is number => sign !== undefined);

if (knownTransactionSigns.length === 0 && !hasOnlyUnknownNonPendingScanDirections) {
return undefined;
}

// 1. If all amounts have the same sign - either all amounts are positive or all amounts are negative.
// We have to do it this way because there can be a case when actions are not available.
// See: https://github.com/Expensify/App/pull/64802#issuecomment-3008944401
const areAmountsSignsTheSame = hasOnlyUnknownNonPendingScanDirections || new Set(knownTransactionSigns).size < 2;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Return undefined when all transaction directions are unknown

When iouActions are unavailable and every loaded transaction has an unknown direction (for example, multiple zero-amount receipt-backed items before scan data resolves), hasOnlyUnknownNonPendingScanDirections makes areAmountsSignsTheSame evaluate to true and the function can return a single sender ID. In that state there is no reliable direction or actor signal, so collapsing to one avatar can incorrectly hide mixed senders; this should stay undefined until at least one transaction direction or actor mapping is known.

Useful? React with 👍 / 👎.


if (!areAmountsSignsTheSame) {
return undefined;
}
}

// 2. If there is only one attendee - we check that by counting unique emails converted to account IDs in the attendees list.
// This is a fallback added because: https://github.com/Expensify/App/pull/64802#issuecomment-3007906310

Expand All @@ -86,13 +257,13 @@ function getReportPreviewSenderID({iouReport, action, chatReport, iouActions, tr
}

// If the action is a 'Send Money' flow, it will only have one transaction, but the person who sent the money is the child manager account, not the child owner account.
const isSendMoneyFlowBasedOnActions = !!iouActions && iouActions.every(isSentMoneyReportAction);
const isSendMoneyFlowBasedOnActions = activeIOUActions.length > 0 && activeIOUActions.every(isSentMoneyReportAction);
// This is used only if there are no IOU actions in the Onyx
// eslint-disable-next-line rulesdir/no-negated-variables
const isSendMoneyFlowBasedOnTransactions =
!!action && action.childMoneyRequestCount === 0 && transactions?.length === 1 && (chatReport ? isDM(chatReport) : policy?.type === CONST.POLICY.TYPE.PERSONAL);

const isSendMoneyFlow = !!iouActions && iouActions?.length > 0 ? isSendMoneyFlowBasedOnActions : isSendMoneyFlowBasedOnTransactions;
const isSendMoneyFlow = activeIOUActions.length > 0 ? isSendMoneyFlowBasedOnActions : isSendMoneyFlowBasedOnTransactions;

const singleAvatarAccountID = isSendMoneyFlow ? action?.childManagerAccountID : action?.childOwnerAccountID;

Expand All @@ -108,14 +279,33 @@ function useReportPreviewSenderID({iouReport, action, chatReport}: {action: Onyx
const [iouActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(shouldFetchData ? iouReport?.reportID : undefined)}`, {
selector: getIOUActionsSelector,
});
const [hasOnceLoadedReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${getNonEmptyStringOnyxID(shouldFetchData ? action?.childReportID : undefined)}`, {
selector: hasOnceLoadedReportActionsSelector,
});
const [isLoadingInitialReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${getNonEmptyStringOnyxID(shouldFetchData ? action?.childReportID : undefined)}`, {
selector: isLoadingInitialReportActionsSelector,
});
const hasFinishedInitialReportActionsLoad = hasOnceLoadedReportActions === true || isLoadingInitialReportActions === false;

const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(shouldFetchData ? action?.childReportID : undefined);
const transactions = useMemo(() => {
if (!shouldFetchData) {
return undefined;
}
return getAllNonDeletedTransactions(reportTransactions, iouActions ?? []);
}, [reportTransactions, iouActions, shouldFetchData]);
const activeMoneyRequestCount = iouReport?.transactionCount ?? action?.childMoneyRequestCount ?? 0;
const allReportTransactions = Object.values(reportTransactions ?? {}).filter((transaction): transaction is Transaction => !!transaction);
const nonDeletedTransactionsIncludingOrphans = getAllNonDeletedTransactions(reportTransactions, iouActions ?? [], false, true);
const filteredTransactions =
nonDeletedTransactionsIncludingOrphans.length < allReportTransactions.length
? getAllNonDeletedTransactions(reportTransactions, iouActions ?? [])
: nonDeletedTransactionsIncludingOrphans;

if (filteredTransactions.length < allReportTransactions.length && filteredTransactions.length < activeMoneyRequestCount) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment why this condition is needed

return nonDeletedTransactionsIncludingOrphans;
}

return filteredTransactions;
}, [reportTransactions, iouActions, shouldFetchData, iouReport?.transactionCount, action?.childMoneyRequestCount]);

const [splits] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(shouldFetchData ? chatReport?.reportID : undefined)}`, {
selector: getSplitsSelector,
Expand All @@ -124,7 +314,7 @@ function useReportPreviewSenderID({iouReport, action, chatReport}: {action: Onyx
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(shouldFetchData ? iouReport?.policyID : undefined)}`);
const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();

return getReportPreviewSenderID({iouReport, action, chatReport, iouActions, transactions, splits, policy, currentUserAccountID});
return getReportPreviewSenderID({iouReport, action, chatReport, iouActions, transactions, splits, policy, currentUserAccountID, hasFinishedInitialReportActionsLoad});
}

export default useReportPreviewSenderID;
Expand Down
1 change: 1 addition & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7696,6 +7696,7 @@ function updateReportPreview(
},
],
childLastMoneyRequestComment: comment || reportPreviewAction?.childLastMoneyRequestComment,
childLastActorAccountID: isPayRequest ? reportPreviewAction?.childLastActorAccountID : deprecatedCurrentUserAccountID,
childMoneyRequestCount: (reportPreviewAction?.childMoneyRequestCount ?? 0) + (isPayRequest ? 0 : 1),
childRecentReceiptTransactionIDs: hasReceipt
? {
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/ReportUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ import {
sortIconsByName,
sortOutstandingReportsBySelected,
temporary_getMoneyRequestOptions,
updateReportPreview,
} from '@libs/ReportUtils';
import {buildOptimisticTransaction} from '@libs/TransactionUtils';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -10766,6 +10767,33 @@ describe('ReportUtils', () => {
});
});

describe('updateReportPreview', () => {
it('refreshes childLastActorAccountID when a new expense request is added', () => {
const chatReport: Report = {
...createRandomReport(100, undefined),
type: CONST.REPORT.TYPE.CHAT,
};

const iouReport: Report = {
...createRandomReport(200, undefined),
parentReportID: '1',
type: CONST.REPORT.TYPE.IOU,
ownerAccountID: 1,
managerID: 2,
};

const reportPreviewAction = buildOptimisticReportPreview(chatReport, iouReport);
const updatedPreviewAction = updateReportPreview(iouReport, reportPreviewAction, false, '', {
transactionID: 'transaction-1',
amount: 0,
created: '2026-05-19 10:00:00',
receipt: {source: 'receipt.jpg'},
} as Transaction);

expect(updatedPreviewAction.childLastActorAccountID).toBe(currentUserAccountID);
});
});

describe('compute (Formula.ts for optimistic report names)', () => {
const mockPolicy: Policy = {
id: 'test-policy-id',
Expand Down
Loading
Loading