diff --git a/src/components/TransactionItemRow/TransactionItemRowRBR.tsx b/src/components/TransactionItemRow/TransactionItemRowRBR.tsx
index b22dd57cadc7..8e0826d56690 100644
--- a/src/components/TransactionItemRow/TransactionItemRowRBR.tsx
+++ b/src/components/TransactionItemRow/TransactionItemRowRBR.tsx
@@ -11,7 +11,7 @@ import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils';
+import {getIOUActionForTransactionID, wasActionTakenByCurrentUser} from '@libs/ReportActionsUtils';
import {isMarkAsCashActionForTransaction} from '@libs/ReportPrimaryActionUtils';
import {isSettled} from '@libs/ReportUtils';
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
@@ -61,23 +61,25 @@ function TransactionItemRowRBRInner({transaction, violations, report, containerS
const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`);
const [cardList] = useOnyx(ONYXKEYS.CARD_LIST);
const icons = useMemoizedLazyExpensifyIcons(['DotIndicator']);
- const transactionThreadId = reportActions ? getIOUActionForTransactionID(Object.values(reportActions ?? {}), transaction.transactionID)?.childReportID : undefined;
+ const iouAction = reportActions ? getIOUActionForTransactionID(Object.values(reportActions ?? {}), transaction.transactionID) : undefined;
+ const transactionThreadId = iouAction?.childReportID;
const [transactionThreadActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadId}`);
const {login: currentUserLogin} = useCurrentUserPersonalDetails();
const isMarkAsCash = parentReport && currentUserLogin && violations ? isMarkAsCashActionForTransaction(currentUserLogin, parentReport, violations, policy) : false;
- const RBRMessages = ViolationsUtils.getRBRMessages(
+ const canEdit = wasActionTakenByCurrentUser(iouAction);
+ const RBRMessages = ViolationsUtils.getRBRMessages({
transaction,
- isSettled(report) ? [] : (violations ?? []),
+ transactionViolations: isSettled(report) ? [] : (violations ?? []),
translate,
missingFieldError,
- Object.values(transactionThreadActions ?? {}),
- policyTags,
+ transactionThreadActions: Object.values(transactionThreadActions ?? {}),
+ tags: policyTags,
companyCardPageURL,
- undefined,
cardList,
- isMarkAsCash,
- );
+ isMarkAsCash: isMarkAsCash || undefined,
+ canEdit,
+ });
const hasHTMLTags = HTML_TAG_PATTERN.test(RBRMessages);
return (
diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts
index d02c1632b001..a3c097019317 100644
--- a/src/libs/OptionsListUtils/index.ts
+++ b/src/libs/OptionsListUtils/index.ts
@@ -66,6 +66,7 @@ import {
getRemovedCardFeedMessage,
getRenamedAction,
getRenamedCardFeedMessage,
+ getReportAction,
getReportActionActorAccountID,
getReportActionHtml,
getReportActionMessageText,
@@ -109,6 +110,7 @@ import {
isTaskAction,
isThreadParentMessage,
isUnapprovedAction,
+ wasActionTakenByCurrentUser,
withDEWRoutedActionsArray,
} from '@libs/ReportActionsUtils';
import {getReportName} from '@libs/ReportNameUtils';
@@ -816,7 +818,9 @@ function getLastMessageTextForReport({
} else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION) {
lastMessageTextFromReport = getExportIntegrationLastMessageText(translate, lastReportAction);
} else if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RECEIPT_SCAN_FAILED) {
- lastMessageTextFromReport = getReportActionMessageText(lastReportAction) || translate('iou.receiptScanningFailed');
+ // RECEIPT_SCAN_FAILED is submitted by Concierge, so use the IOU action to determine edit permission
+ const iouAction = getReportAction(report?.parentReportID, report?.parentReportActionID);
+ lastMessageTextFromReport = translate('violations.smartscanFailed', {canEdit: wasActionTakenByCurrentUser(iouAction)});
} else if (lastReportAction?.actionName && isOldDotReportAction(lastReportAction)) {
lastMessageTextFromReport = getMessageOfOldDotReportAction(translate, lastReportAction, false);
} else if (isActionableJoinRequest(lastReportAction)) {
diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts
index 8a979372588c..202f1b3758f3 100644
--- a/src/libs/ReportNameUtils.ts
+++ b/src/libs/ReportNameUtils.ts
@@ -70,8 +70,8 @@ import {
getRemovedCardFeedMessage,
getRenamedAction,
getRenamedCardFeedMessage,
+ getReportAction,
getReportActionMessage as getReportActionMessageFromActionsUtils,
- getReportActionMessageText,
getReportActionText,
getSettlementAccountLockedMessage,
getSubmitsToUpdateMessage,
@@ -114,6 +114,7 @@ import {
isTagModificationAction,
isTransactionThread,
isUnapprovedAction,
+ wasActionTakenByCurrentUser,
} from './ReportActionsUtils';
// eslint-disable-next-line import/no-cycle
import {
@@ -472,7 +473,12 @@ function computeReportNameBasedOnReportAction(
});
}
if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RECEIPT_SCAN_FAILED) {
- return getReportActionMessageText(parentReportAction) || translate('iou.receiptScanningFailed');
+ // RECEIPT_SCAN_FAILED is submitted by Concierge, so use the IOU action to determine edit permission
+ let iouAction = getReportAction(report?.parentReportID, report?.parentReportActionID);
+ if (!isActionOfType(iouAction, CONST.REPORT.ACTIONS.TYPE.IOU)) {
+ iouAction = getReportAction(parentReport?.parentReportID, parentReport?.parentReportActionID);
+ }
+ return translate('violations.smartscanFailed', {canEdit: wasActionTakenByCurrentUser(iouAction)});
}
if (isReimbursementDeQueuedOrCanceledAction(parentReportAction)) {
diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts
index fe9b3faf5088..b4e3aa77ffc4 100644
--- a/src/libs/Violations/ViolationsUtils.ts
+++ b/src/libs/Violations/ViolationsUtils.ts
@@ -835,18 +835,31 @@ const ViolationsUtils = {
return Number(violation.data?.formattedLimit?.replace(CONST.VIOLATION_LIMIT_REGEX, ''));
},
- getRBRMessages(
- transaction: Transaction,
- transactionViolations: TransactionViolation[],
- translate: LocaleContextProps['translate'],
- missingFieldError?: string,
- transactionThreadActions?: ReportAction[],
- tags?: PolicyTagLists,
- companyCardPageURL?: string,
- connectionLink?: string,
- cardList?: CardList,
- isMarkAsCash?: boolean,
- ): string {
+ getRBRMessages({
+ transaction,
+ transactionViolations,
+ translate,
+ missingFieldError,
+ transactionThreadActions,
+ tags,
+ companyCardPageURL,
+ connectionLink,
+ cardList,
+ isMarkAsCash,
+ canEdit = true,
+ }: {
+ transaction: Transaction;
+ transactionViolations: TransactionViolation[];
+ translate: LocaleContextProps['translate'];
+ missingFieldError?: string;
+ transactionThreadActions?: ReportAction[];
+ tags?: PolicyTagLists;
+ companyCardPageURL?: string;
+ connectionLink?: string;
+ cardList?: CardList;
+ isMarkAsCash?: boolean;
+ canEdit?: boolean;
+ }): string {
const errorMessages = extractErrorMessages(transaction?.errors ?? {}, transactionThreadActions?.filter((e) => !!e.errors) ?? [], translate);
const filteredViolations = filterReceiptViolations(transactionViolations);
@@ -861,6 +874,7 @@ const ViolationsUtils = {
const message = ViolationsUtils.getViolationTranslation({
violation,
translate,
+ canEdit,
tags,
companyCardPageURL,
connectionLink,
diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx
index 4b60a6695d66..921cf8353744 100644
--- a/src/pages/inbox/report/PureReportActionItem.tsx
+++ b/src/pages/inbox/report/PureReportActionItem.tsx
@@ -795,7 +795,12 @@ function PureReportActionItem({
/>
);
} else if (isSimpleMessageAction(action)) {
- children = ;
+ children = (
+
+ );
} else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.FORWARDED)) {
const wasAutoForwarded = getOriginalMessage(action)?.automaticAction ?? false;
if (wasAutoForwarded) {
diff --git a/src/pages/inbox/report/actionContents/SimpleMessageContent.tsx b/src/pages/inbox/report/actionContents/SimpleMessageContent.tsx
index 1737daf4c77f..e768dbb1e278 100644
--- a/src/pages/inbox/report/actionContents/SimpleMessageContent.tsx
+++ b/src/pages/inbox/report/actionContents/SimpleMessageContent.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import {
getActionableCard3DSTransactionApprovalMessage,
@@ -8,11 +9,12 @@ import {
getMessageOfOldDotReportAction,
getOriginalMessage,
getRemovedFromApprovalChainMessage,
- getReportActionMessageText,
+ getReportAction,
getReportActionText,
isActionOfType,
isRejectedAction,
isUnapprovedAction,
+ wasActionTakenByCurrentUser,
} from '@libs/ReportActionsUtils';
import {getDeletedTransactionMessage, getPolicyChangeMessage} from '@libs/ReportUtils';
import ReportActionItemBasicMessage from '@pages/inbox/report/ReportActionItemBasicMessage';
@@ -21,6 +23,7 @@ import type * as OnyxTypes from '@src/types/onyx';
type SimpleMessageContentProps = {
action: OnyxTypes.ReportAction;
+ report: OnyxEntry;
};
const SIMPLE_MESSAGE_ACTION_TYPES = new Set([
@@ -48,7 +51,7 @@ function isSimpleMessageAction(action: OnyxTypes.ReportAction): boolean {
return SIMPLE_MESSAGE_ACTION_TYPES.has(action.actionName) || isUnapprovedAction(action) || isRejectedAction(action);
}
-function SimpleMessageContent({action}: SimpleMessageContentProps) {
+function SimpleMessageContent({action, report}: SimpleMessageContentProps) {
const {translate} = useLocalize();
if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.MARKED_REIMBURSED)) {
@@ -91,8 +94,9 @@ function SimpleMessageContent({action}: SimpleMessageContentProps) {
return ;
}
if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.RECEIPT_SCAN_FAILED)) {
- const htmlMessage = getReportActionMessageText(action) || translate('iou.receiptScanningFailed');
- return ;
+ // RECEIPT_SCAN_FAILED is submitted by Concierge, so use the IOU action to determine edit permission
+ const iouAction = getReportAction(report?.parentReportID, report?.parentReportActionID);
+ return ;
}
if (isUnapprovedAction(action)) {
return ;
diff --git a/tests/ui/PureReportActionItemTest.tsx b/tests/ui/PureReportActionItemTest.tsx
index ab2f4fe44a67..9f6d060e0f8c 100644
--- a/tests/ui/PureReportActionItemTest.tsx
+++ b/tests/ui/PureReportActionItemTest.tsx
@@ -902,27 +902,99 @@ describe('PureReportActionItem', () => {
expect(screen.getByText(translateLocal(translationKey))).toBeOnTheScreen();
});
- it('RECEIPT_SCAN_FAILED action shows message from action data', async () => {
- // Given a RECEIPT_SCAN_FAILED message with a html message from server.
- // Then verify server message is rendered.
+ it('RECEIPT_SCAN_FAILED action shows submitter message when current user is the expense submitter', async () => {
+ const parentReportID = 'parentReport1';
+ const parentReportActionID = 'iouAction1';
+
+ await act(async () => {
+ await Onyx.merge(ONYXKEYS.SESSION, {accountID: ACTOR_ACCOUNT_ID});
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, {
+ [parentReportActionID]: {
+ reportActionID: parentReportActionID,
+ actorAccountID: ACTOR_ACCOUNT_ID,
+ actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
+ created: '2025-07-12 09:03:17.653',
+ message: [{type: 'COMMENT', html: '', text: ''}],
+ originalMessage: {type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, amount: 100, currency: 'USD'},
+ },
+ });
+ });
+ await waitForBatchedUpdatesWithAct();
+
+ const report = {reportID: 'scanReport1', parentReportID, parentReportActionID};
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.RECEIPT_SCAN_FAILED, {});
- action.message = [
- {
- type: 'COMMENT',
- html: "the date couldn't be read from this receipt. Please enter it manually.",
- text: "the date couldn't be read from this receipt. Please enter it manually.",
- },
- ];
- renderItemWithAction(action);
+
+ render(
+
+
+
+
+
+
+
+
+ ,
+ );
await waitForBatchedUpdatesWithAct();
- expect(screen.getByText("the date couldn't be read from this receipt. Please enter it manually.")).toBeOnTheScreen();
- // Given an RECEIPT_SCAN_FAILED with no server side message
- // Then verify generic translation phrase is rendered
- action.message = [{type: 'COMMENT', html: '', text: ''}];
- renderItemWithAction(action);
+ expect(screen.getByText(translateLocal('violations.smartscanFailed', {canEdit: true}))).toBeOnTheScreen();
+ });
+
+ it('RECEIPT_SCAN_FAILED action shows non-submitter message when current user is not the expense submitter', async () => {
+ const parentReportID = 'parentReport2';
+ const parentReportActionID = 'iouAction2';
+ const OTHER_ACCOUNT_ID = 999999;
+
+ await act(async () => {
+ await Onyx.merge(ONYXKEYS.SESSION, {accountID: ACTOR_ACCOUNT_ID});
+ await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, {
+ [parentReportActionID]: {
+ reportActionID: parentReportActionID,
+ actorAccountID: OTHER_ACCOUNT_ID,
+ actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
+ created: '2025-07-12 09:03:17.653',
+ message: [{type: 'COMMENT', html: '', text: ''}],
+ originalMessage: {type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, amount: 100, currency: 'USD'},
+ },
+ });
+ });
+ await waitForBatchedUpdatesWithAct();
+
+ const report = {reportID: 'scanReport2', parentReportID, parentReportActionID};
+ const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.RECEIPT_SCAN_FAILED, {});
+
+ render(
+
+
+
+
+
+
+
+
+ ,
+ );
await waitForBatchedUpdatesWithAct();
- expect(screen.getByText(translateLocal('iou.receiptScanningFailed'))).toBeOnTheScreen();
+
+ expect(screen.getByText(translateLocal('violations.smartscanFailed', {canEdit: false}))).toBeOnTheScreen();
});
it('HOLD_COMMENT action renders via ReportActionItemBasicMessage', async () => {
diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts
index 12cdc011c1cc..a60f414ac5ba 100644
--- a/tests/unit/ViolationUtilsTest.ts
+++ b/tests/unit/ViolationUtilsTest.ts
@@ -1615,14 +1615,20 @@ describe('getRBRMessages', () => {
it('should return all violations and missing field error', () => {
const missingFieldError = 'Missing required field';
- const result = ViolationsUtils.getRBRMessages(mockTransaction, mockViolations, translateLocal, missingFieldError, []);
+ const result = ViolationsUtils.getRBRMessages({
+ transaction: mockTransaction,
+ transactionViolations: mockViolations,
+ translate: translateLocal,
+ missingFieldError,
+ transactionThreadActions: [],
+ });
const expectedResult = `Missing required field. ${translateLocal('violations.missingCategory')}. ${translateLocal('violations.missingTag')}.`;
expect(result).toBe(expectedResult);
});
it('should filter out empty strings', () => {
- const result = ViolationsUtils.getRBRMessages(mockTransaction, mockViolations, translateLocal, undefined, []);
+ const result = ViolationsUtils.getRBRMessages({transaction: mockTransaction, transactionViolations: mockViolations, translate: translateLocal, transactionThreadActions: []});
const expectedResult = `${translateLocal('violations.missingCategory')}. ${translateLocal('violations.missingTag')}.`;
expect(result).toBe(expectedResult);