From feb5f3d72ba76e5b50a915b7ba709d1a04d63386 Mon Sep 17 00:00:00 2001 From: Jules Date: Thu, 19 Mar 2026 13:06:00 -0700 Subject: [PATCH] Revert "[NO QA] Remove ONYXKEYS.COLLECTION.REPORT_VIOLATIONS" --- src/CONST/index.ts | 4 + src/ONYXKEYS.ts | 2 + .../MoneyRequestViewReportFields.tsx | 8 +- .../ReportActionItem/MoneyReportView.tsx | 6 +- .../TransactionPreviewContent.tsx | 4 +- src/libs/ReportUtils.ts | 24 ++++- src/libs/TransactionPreviewUtils.ts | 16 +++- src/libs/actions/IOU/index.ts | 76 ++++++++++++++- src/libs/actions/Report/index.ts | 25 ++++- src/pages/Debug/Report/DebugReportPage.tsx | 21 +++- src/pages/EditReportFieldPage.tsx | 2 + src/types/onyx/ReportViolation.ts | 21 ++++ src/types/onyx/ReportViolationName.ts | 10 -- src/types/onyx/index.ts | 5 +- tests/unit/TransactionPreviewUtils.test.ts | 95 +++++++++++++++++++ 15 files changed, 294 insertions(+), 25 deletions(-) create mode 100644 src/types/onyx/ReportViolation.ts delete mode 100644 src/types/onyx/ReportViolationName.ts diff --git a/src/CONST/index.ts b/src/CONST/index.ts index c457a7a913029..1f884015b11f3 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6348,6 +6348,10 @@ const CONST = { RBR_MESSAGE_MAX_CHARACTERS_FOR_PREVIEW: 40, }, + REPORT_VIOLATIONS_EXCLUDED_FIELDS: { + TEXT_TITLE: 'text_title', + }, + /** Context menu types */ CONTEXT_MENU_TYPES: { LINK: 'LINK', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 87f5f6c8be379..152fec8fbd875 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -713,6 +713,7 @@ const ONYXKEYS = { REPORT_USER_IS_TYPING: 'reportUserIsTyping_', PENDING_CONCIERGE_RESPONSE: 'pendingConciergeResponse_', REPORT_USER_IS_LEAVING_ROOM: 'reportUserIsLeavingRoom_', + REPORT_VIOLATIONS: 'reportViolations_', SECURITY_GROUP: 'securityGroup_', TRANSACTION: 'transactions_', TRANSACTION_VIOLATIONS: 'transactionViolations_', @@ -1197,6 +1198,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: OnyxTypes.ReportUserIsTyping; [ONYXKEYS.COLLECTION.PENDING_CONCIERGE_RESPONSE]: OnyxTypes.PendingConciergeResponse; [ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM]: boolean; + [ONYXKEYS.COLLECTION.REPORT_VIOLATIONS]: OnyxTypes.ReportViolations; [ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup; [ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction; diff --git a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx index 564a6a121327b..0c4abfbdf46b4 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx @@ -4,6 +4,7 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearReportFieldKeyErrors} from '@libs/actions/Report'; import {resolveReportFieldValue} from '@libs/Formula'; @@ -22,6 +23,7 @@ import { } from '@libs/ReportUtils'; import type {ThemeStyles} from '@styles/index'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, PolicyReportField, Report, ReportViolationName} from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; @@ -80,6 +82,8 @@ function ReportFieldView(reportField: EnrichedPolicyReportField, report: OnyxEnt function MoneyRequestViewReportFields({report, policy, isCombinedReport = false, pendingAction}: MoneyRequestViewReportFieldsProps) { const styles = useThemeStyles(); + const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report?.reportID}`); + const sortedPolicyReportFields = useMemo((): EnrichedPolicyReportField[] => { const {fieldValues, fieldsByName} = getReportFieldMaps(report, policy?.fieldList ?? {}); const fields = Object.values(fieldsByName); @@ -94,7 +98,7 @@ function MoneyRequestViewReportFields({report, policy, isCombinedReport = false, const isDeletedFormulaField = field.type === CONST.REPORT_FIELD_TYPES.FORMULA && field.deletable; const fieldKey = getReportFieldKey(field.fieldID); - const violation = isFieldDisabled ? undefined : getFieldViolation(field); + const violation = isFieldDisabled ? undefined : getFieldViolation(violations, field); const violationTranslation = getFieldViolationTranslation(field, violation); return { @@ -106,7 +110,7 @@ function MoneyRequestViewReportFields({report, policy, isCombinedReport = false, violationTranslation, }; }); - }, [policy, report]); + }, [policy, report, violations]); const enabledReportFields = sortedPolicyReportFields.filter( (reportField) => !isReportFieldDisabled(report, reportField, policy) || reportField.type === CONST.REPORT_FIELD_TYPES.FORMULA, diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 290fca2804082..ed8946cc980ec 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -13,6 +13,7 @@ import UnreadActionIndicator from '@components/UnreadActionIndicator'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; import useReportTransactions from '@hooks/useReportTransactions'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -45,6 +46,7 @@ import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import {clearReportFieldKeyErrors} from '@src/libs/actions/Report'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, Report} from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; @@ -113,6 +115,8 @@ function MoneyReportView({ StyleUtils.getColorStyle(theme.textSupporting), ]; + const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report?.reportID}`); + const {sortedPolicyReportFields, fieldValues, fieldsByName} = useMemo(() => { const {fieldValues: values, fieldsByName: byName} = getReportFieldMaps(report, policy?.fieldList ?? {}); const sorted = Object.values(byName) @@ -172,7 +176,7 @@ function MoneyReportView({ const isFieldDisabled = isReportFieldDisabledForUser(report, reportField, policy); const fieldKey = getReportFieldKey(reportField.fieldID); - const violation = isFieldDisabled ? undefined : getFieldViolation(reportField); + const violation = isFieldDisabled ? undefined : getFieldViolation(violations, reportField); const violationTranslation = getFieldViolationTranslation(reportField, violation); return ( diff --git a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx index 357002985a073..b121ee9927c0c 100644 --- a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx +++ b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx @@ -73,6 +73,7 @@ function TransactionPreviewContent({ const styles = useThemeStyles(); const {translate} = useLocalize(); const {environmentURL} = useEnvironment(); + const [reportViolations] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${getNonEmptyStringOnyxID(report?.reportID)}`); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`); const isParentPolicyExpenseChat = isPolicyExpenseChat(chatReport); const transactionDetails = useMemo>( @@ -114,8 +115,9 @@ function TransactionPreviewContent({ currentUserEmail, currentUserAccountID, reportActions, + reportViolations, }), - [areThereDuplicates, transactionPreviewCommonArguments, isReportAPolicyExpenseChat, currentUserEmail, currentUserAccountID, reportActions], + [areThereDuplicates, transactionPreviewCommonArguments, isReportAPolicyExpenseChat, currentUserEmail, currentUserAccountID, reportActions, reportViolations], ); const {shouldShowRBR, shouldShowMerchant, shouldShowSplitShare, shouldShowTag, shouldShowCategory, shouldShowSkeleton, shouldShowDescription} = conditionals; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9e083ab638b47..56d1c416324fa 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -56,6 +56,7 @@ import type { ReportMetadata, ReportNameValuePairs, ReportViolationName, + ReportViolations, Task, Transaction, TransactionViolation, @@ -9317,6 +9318,13 @@ function hasAnyViolations( ); } +function hasReportViolations(reportViolations: OnyxEntry) { + if (!reportViolations) { + return false; + } + return Object.values(reportViolations ?? {}).some((violations) => !isEmptyObject(violations)); +} + /** * Checks if submission should be blocked due to strict policy rules being enabled and violations present. * When a user's domain has "strictly enforce workspace rules" enabled, they cannot submit reports with violations. @@ -12409,12 +12417,23 @@ function getChatUsedForOnboarding(onboardingValue: OnyxEntry, concie /** * Checks if given field has any violations and returns name of the first encountered one */ -function getFieldViolation(reportField: PolicyReportField): ReportViolationName | undefined { +function getFieldViolation(violations: OnyxEntry, reportField: PolicyReportField): ReportViolationName | undefined { if (!reportField) { return undefined; } - return (reportField.value ?? reportField.defaultValue) ? undefined : CONST.REPORT_VIOLATIONS.FIELD_REQUIRED; + if (!violations) { + return (reportField.value ?? reportField.defaultValue) ? undefined : CONST.REPORT_VIOLATIONS.FIELD_REQUIRED; + } + + const fieldViolation = Object.values(CONST.REPORT_VIOLATIONS).find((violation) => !!violations[violation] && violations[violation][reportField.fieldID]); + + // If the field has no value or no violation, we return 'fieldRequired' violation + if (!fieldViolation) { + return reportField.value ? undefined : CONST.REPORT_VIOLATIONS.FIELD_REQUIRED; + } + + return fieldViolation; } /** @@ -13502,6 +13521,7 @@ export { getMostRecentlyVisitedReport, getSourceIDFromReportAction, getIntegrationNameFromExportMessage, + hasReportViolations, isPayAtEndExpenseReport, getApprovalChain, isIndividualInvoiceRoom, diff --git a/src/libs/TransactionPreviewUtils.ts b/src/libs/TransactionPreviewUtils.ts index ce01f0d59bf9f..349df6f990ef0 100644 --- a/src/libs/TransactionPreviewUtils.ts +++ b/src/libs/TransactionPreviewUtils.ts @@ -11,7 +11,16 @@ import {convertToDisplayString} from './CurrencyUtils'; import DateUtils from './DateUtils'; import {hasDynamicExternalWorkflow} from './PolicyUtils'; import {getMostRecentActiveDEWSubmitFailedAction, getOriginalMessage, isDynamicExternalWorkflowSubmitFailedAction, isMessageDeleted, isMoneyRequestAction} from './ReportActionsUtils'; -import {hasActionWithErrorsForTransaction, hasReceiptError, isPaidGroupPolicyExpenseReport, isPaidGroupPolicy as isPaidGroupPolicyUtil, isReportApproved, isSettled} from './ReportUtils'; +import { + hasActionWithErrorsForTransaction, + hasReceiptError, + hasReportViolations, + isPaidGroupPolicyExpenseReport, + isPaidGroupPolicy as isPaidGroupPolicyUtil, + isReportApproved, + isReportOwner, + isSettled, +} from './ReportUtils'; import type {TransactionDetails} from './ReportUtils'; import StringUtils from './StringUtils'; import { @@ -359,6 +368,7 @@ function getTransactionPreviewTextAndTranslationPaths({ } function createTransactionPreviewConditionals({ + reportViolations, iouReport, policy, transaction, @@ -372,6 +382,7 @@ function createTransactionPreviewConditionals({ currentUserAccountID, reportActions, }: { + reportViolations: OnyxEntry; iouReport: OnyxEntry; policy: OnyxEntry; transaction: OnyxEntry | undefined; @@ -422,7 +433,8 @@ function createTransactionPreviewConditionals({ (violation) => violation.name === CONST.VIOLATIONS.MODIFIED_AMOUNT && (violation.type === CONST.VIOLATION_TYPES.VIOLATION || violation.type === CONST.VIOLATION_TYPES.NOTICE), )); const hasErrorOrOnHold = hasFieldErrors || (!isFullySettled && !isFullyApproved && isTransactionOnHold); - const hasReportViolationsOrActionErrors = hasActionWithErrorsForTransaction(iouReport?.reportID, transaction, reportActions); + const hasReportViolationsOrActionErrors = + (isReportOwner(iouReport) && hasReportViolations(reportViolations)) || hasActionWithErrorsForTransaction(iouReport?.reportID, transaction, reportActions); const isDEWSubmitFailed = hasDynamicExternalWorkflow(policy) && !!getMostRecentActiveDEWSubmitFailedAction(reportActions); const shouldShowRBR = hasAnyViolations || hasErrorOrOnHold || hasReportViolationsOrActionErrors || hasReceiptError(transaction) || isDEWSubmitFailed; diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 2f790a769798e..fdc5fe67e3d32 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -79,6 +79,7 @@ import { getSubmitToAccountID, hasDependentTags, hasDynamicExternalWorkflow, + isControlPolicy, isDelayedSubmissionEnabled, isPaidGroupPolicy, isPolicyAdmin, @@ -1775,6 +1776,39 @@ function getReceiptError( ); } +/** Helper function to get optimistic fields violations onyx data */ +function getFieldViolationsOnyxData(iouReport: OnyxTypes.Report): OnyxData { + const missingFields: OnyxTypes.ReportFieldsViolations = {}; + const excludedFields = Object.values(CONST.REPORT_VIOLATIONS_EXCLUDED_FIELDS) as string[]; + + for (const field of Object.values(iouReport.fieldList ?? {})) { + if (excludedFields.includes(field.fieldID) || !!field.value || !!field.defaultValue) { + continue; + } + // in case of missing field violation the empty object is indicator. + missingFields[field.fieldID] = {}; + } + + return { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${iouReport.reportID}`, + value: { + fieldRequired: missingFields, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${iouReport.reportID}`, + value: null, + }, + ], + }; +} + type BuildOnyxDataForTestDriveIOUParams = { transaction: OnyxTypes.Transaction; iouOptimisticParams: MoneyRequestOptimisticParams['iou']; @@ -2545,6 +2579,7 @@ type BuildOnyxDataForTrackExpenseParams = { chat: {report: OnyxInputValue; previewAction: OnyxInputValue}; iou: {report: OnyxInputValue; createdAction: OptimisticCreatedReportAction; action: OptimisticIOUReportAction}; transactionParams: {transaction: OnyxTypes.Transaction; threadReport: OptimisticChatReport | null; threadCreatedReportAction: OptimisticCreatedReportAction | null}; + policyParams: {policy?: OnyxInputValue; tagList?: OnyxInputValue; categories?: OnyxInputValue}; shouldCreateNewMoneyRequestReport: boolean; existingTransactionThreadReportID?: string; actionableTrackExpenseWhisper?: OnyxInputValue; @@ -2561,13 +2596,15 @@ type BuildOnyxDataForTrackExpenseKeys = | typeof ONYXKEYS.COLLECTION.TRANSACTION | typeof ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE | typeof ONYXKEYS.COLLECTION.SNAPSHOT - | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS; + | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + | typeof ONYXKEYS.COLLECTION.REPORT_VIOLATIONS; /** Builds the Onyx data for track expense */ function buildOnyxDataForTrackExpense({ chat, iou, transactionParams, + policyParams = {}, shouldCreateNewMoneyRequestReport, existingTransactionThreadReportID, actionableTrackExpenseWhisper, @@ -2579,6 +2616,8 @@ function buildOnyxDataForTrackExpense({ const {report: chatReport, previewAction: reportPreviewAction} = chat; const {report: iouReport, createdAction: iouCreatedAction, action: iouAction} = iou; const {transaction, threadReport: transactionThreadReport, threadCreatedReportAction: transactionThreadCreatedReportAction} = transactionParams; + const {policy, tagList: policyTagList, categories: policyCategories} = policyParams; + const isScanRequest = isScanRequestTransactionUtils(transaction); const isDistanceRequest = isDistanceRequestTransactionUtils(transaction); const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); @@ -2954,6 +2993,38 @@ function buildOnyxDataForTrackExpense({ onyxData.successData?.push(...searchUpdate.successData); } } + + // We don't need to compute violations unless we're on a paid policy + if (!policy || !isPaidGroupPolicy(policy) || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID) { + return onyxData; + } + + const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( + transaction, + [], + policy, + policyTagList ?? {}, + policyCategories ?? {}, + hasDependentTags(policy, policyTagList ?? {}), + false, + ); + + if (violationsOnyxData) { + onyxData.optimisticData?.push(violationsOnyxData); + onyxData.failureData?.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + value: [], + }); + } + + // Show field violations only for control policies + if (isControlPolicy(policy) && iouReport) { + const {optimisticData: fieldViolationsOptimisticData, failureData: fieldViolationsFailureData} = getFieldViolationsOnyxData(iouReport); + onyxData.optimisticData?.push(...(fieldViolationsOptimisticData ?? [])); + onyxData.failureData?.push(...(fieldViolationsFailureData ?? [])); + } + return onyxData; } @@ -3646,7 +3717,7 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T isSelfTourViewed, } = params; const {payeeAccountID = userAccountID, payeeEmail = currentUserEmail, participant} = participantParams; - const {policy} = policyParams; + const {policy, policyCategories, policyTagList} = policyParams; const { comment, amount, @@ -3941,6 +4012,7 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T threadCreatedReportAction: optimisticCreatedActionForTransactionThread, threadReport: optimisticTransactionThread ?? {}, }, + policyParams: {policy, tagList: policyTagList, categories: policyCategories}, shouldCreateNewMoneyRequestReport, actionableTrackExpenseWhisper, retryParams, diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index d48091794a89e..84cb2047313d1 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -127,6 +127,7 @@ import { getChatByParticipants, getChildReportNotificationPreference, getDefaultNotificationPreferenceForReport, + getFieldViolation, getLastVisibleMessage, getNextApproverAccountID, getOptimisticDataForAncestors, @@ -210,6 +211,7 @@ import type { ReportActionReactions, ReportNextStepDeprecated, ReportUserIsTyping, + ReportViolations, Transaction, TransactionViolations, VisibleReportActionsDerivedValue, @@ -3075,6 +3077,7 @@ function updateReportField({ email, hasViolationsParam, recentlyUsedReportFields, + reportViolations, shouldFixViolations = false, }: { report: Report; @@ -3086,10 +3089,12 @@ function updateReportField({ email: string; hasViolationsParam: boolean; recentlyUsedReportFields: OnyxEntry; + reportViolations: OnyxEntry; shouldFixViolations: boolean | undefined; }) { const reportID = report.reportID; const fieldKey = getReportFieldKey(reportField.fieldID); + const fieldViolation = getFieldViolation(reportViolations, reportField); const recentlyUsedValues = recentlyUsedReportFields?.[fieldKey] ?? []; const optimisticChangeFieldAction = buildOptimisticChangeFieldAction(reportField, previousReportField); @@ -3119,7 +3124,13 @@ function updateReportField({ }); const optimisticData: Array< - OnyxUpdate + OnyxUpdate< + | typeof ONYXKEYS.COLLECTION.REPORT + | typeof ONYXKEYS.COLLECTION.NEXT_STEP + | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS + | typeof ONYXKEYS.COLLECTION.REPORT_VIOLATIONS + | typeof ONYXKEYS.RECENTLY_USED_REPORT_FIELDS + > > = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -3149,6 +3160,18 @@ function updateReportField({ }, ]; + if (fieldViolation) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${reportID}`, + value: { + [fieldViolation]: { + [reportField.fieldID]: null, + }, + }, + }); + } + if (reportField.type === 'dropdown' && reportField.value) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, diff --git a/src/pages/Debug/Report/DebugReportPage.tsx b/src/pages/Debug/Report/DebugReportPage.tsx index 705328f9373ae..1c16568608515 100644 --- a/src/pages/Debug/Report/DebugReportPage.tsx +++ b/src/pages/Debug/Report/DebugReportPage.tsx @@ -21,7 +21,7 @@ import DebugTabNavigator from '@libs/Navigation/DebugTabNavigator'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {DebugParamList} from '@libs/Navigation/types'; -import {getViolatingReportIDForRBRInLHN} from '@libs/ReportUtils'; +import {getViolatingReportIDForRBRInLHN, hasReportViolations, isReportOwner} from '@libs/ReportUtils'; import DebugDetails from '@pages/Debug/DebugDetails'; import DebugJSON from '@pages/Debug/DebugJSON'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -59,6 +59,7 @@ function DebugReportPage({ const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const [reportViolations] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${reportID}`); const reportAttributesSelector = useCallback((attributes: OnyxEntry) => attributes?.reports?.[reportID], [reportID]); const [reportAttributes] = useOnyx( ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, @@ -82,7 +83,8 @@ function DebugReportPage({ } const shouldDisplayViolations = !!getViolatingReportIDForRBRInLHN(report, transactionViolations); - const hasViolations = !!shouldDisplayViolations; + const shouldDisplayReportViolations = isReportOwner(report) && hasReportViolations(reportViolations); + const hasViolations = !!shouldDisplayViolations || shouldDisplayReportViolations; const {reason: reasonGBR, reportAction: reportActionGBR} = DebugUtils.getReasonAndReportActionForGBRInLHNRow(report, isReportArchived) ?? {}; const {reason: reasonRBR, reportAction: reportActionRBR} = DebugUtils.getReasonAndReportActionForRBRInLHNRow( @@ -151,7 +153,20 @@ function DebugReportPage({ : undefined, }, ]; - }, [report, transactionViolations, isReportArchived, chatReport, reportActions, transactions, reportAttributes?.reportErrors, betas, priorityMode, draftComment, translate]); + }, [ + report, + transactionViolations, + reportViolations, + isReportArchived, + chatReport, + reportActions, + transactions, + reportAttributes?.reportErrors, + betas, + priorityMode, + draftComment, + translate, + ]); const icons = useMemoizedLazyExpensifyIcons(['Eye'] as const); diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index c0dfedbef248f..7ff09c307da9e 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -68,6 +68,7 @@ function EditReportFieldPage({route}: EditReportFieldPageProps) { const session = useSession(); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const hasViolations = hasViolationsReportUtils(report?.reportID, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); + const [reportViolations] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${reportID}`); const {translate} = useLocalize(); const {showConfirmModal} = useConfirmModal(); const icons = useMemoizedLazyExpensifyIcons(['Trashcan'] as const); @@ -141,6 +142,7 @@ function EditReportFieldPage({route}: EditReportFieldPageProps) { email: session?.email ?? '', hasViolationsParam: hasViolations, recentlyUsedReportFields, + reportViolations, shouldFixViolations: hasOtherViolations ?? false, }); } diff --git a/src/types/onyx/ReportViolation.ts b/src/types/onyx/ReportViolation.ts new file mode 100644 index 0000000000000..fae385cc674d9 --- /dev/null +++ b/src/types/onyx/ReportViolation.ts @@ -0,0 +1,21 @@ +import type {EmptyObject, ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +/** + * Names of violations. + * Derived from `CONST.VIOLATIONS` to maintain a single source of truth. + */ +type ReportViolationName = ValueOf; + +/** + * Keys of this object are IDs of field that has violations + */ +type ReportFieldsViolations = Record; + +/** + * Report Violation model + */ +type ReportViolations = Record; + +export type {ReportViolationName, ReportFieldsViolations}; +export default ReportViolations; diff --git a/src/types/onyx/ReportViolationName.ts b/src/types/onyx/ReportViolationName.ts deleted file mode 100644 index 1635ff727d102..0000000000000 --- a/src/types/onyx/ReportViolationName.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type {ValueOf} from 'type-fest'; -import type CONST from '@src/CONST'; - -/** - * Names of violations. - * Derived from `CONST.VIOLATIONS` to maintain a single source of truth. - */ -type ReportViolationName = ValueOf; - -export default ReportViolationName; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 4a69055dc3590..d6be837a45fda 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -134,7 +134,8 @@ import type ReportNameValuePairs from './ReportNameValuePairs'; import type LastSearchParams from './ReportNavigation'; import type ReportNextStepDeprecated from './ReportNextStepDeprecated'; import type ReportUserIsTyping from './ReportUserIsTyping'; -import type ReportViolationName from './ReportViolationName'; +import type {ReportFieldsViolations, ReportViolationName} from './ReportViolation'; +import type ReportViolations from './ReportViolation'; import type Request from './Request'; import type {AnyRequest} from './Request'; import type Response from './Response'; @@ -280,6 +281,8 @@ export type { ReportMetadata, ReportNextStepDeprecated, ReportViolationName, + ReportViolations, + ReportFieldsViolations, ReportLayoutGroupBy, GroupedTransactions, AnyRequest, diff --git a/tests/unit/TransactionPreviewUtils.test.ts b/tests/unit/TransactionPreviewUtils.test.ts index 2d5fee83289f6..184d9e0a1b39a 100644 --- a/tests/unit/TransactionPreviewUtils.test.ts +++ b/tests/unit/TransactionPreviewUtils.test.ts @@ -13,6 +13,7 @@ import CONST from '@src/CONST'; import * as ReportUtils from '@src/libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportActions, Transaction} from '@src/types/onyx'; +import type ReportViolations from '@src/types/onyx/ReportViolation'; import createRandomPolicy from '../utils/collections/policies'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -513,6 +514,100 @@ describe('TransactionPreviewUtils', () => { expect(result.shouldShowRBR).toBeFalsy(); }); + it('should show RBR when report has violations and user is the report owner', () => { + const reportID = basicProps.iouReport.reportID || '1'; + const iouReport = { + ...basicProps.iouReport, + reportID, + ownerAccountID: currentUserAccountID, + }; + + const reportViolations: ReportViolations = { + fieldRequired: { + field1: {}, + field2: {}, + }, + } as unknown as ReportViolations; + + const functionArgs = { + ...basicProps, + iouReport, + reportViolations, + violations: [], + transaction: {...basicProps.transaction, errors: undefined, errorFields: undefined}, + }; + + const result = createTransactionPreviewConditionals(functionArgs); + expect(result.shouldShowRBR).toBeTruthy(); + }); + + it('should not show RBR from report violations when user is not the report owner', () => { + const reportID = basicProps.iouReport.reportID || '1'; + const otherUserAccountID = 888; + const iouReport = { + ...basicProps.iouReport, + reportID, + ownerAccountID: otherUserAccountID, + }; + + // First, test without violations + const functionArgsWithoutViolations = { + ...basicProps, + iouReport, + reportViolations: undefined, + violations: [], + transaction: {...basicProps.transaction, errors: undefined, errorFields: undefined}, + }; + + const resultWithoutViolations = createTransactionPreviewConditionals(functionArgsWithoutViolations); + const shouldShowRBRWithoutViolations = resultWithoutViolations.shouldShowRBR; + + // Then, test with violations + const reportViolations: ReportViolations = { + fieldRequired: { + field1: {}, + }, + } as unknown as ReportViolations; + + const functionArgsWithViolations = { + ...basicProps, + iouReport, + reportViolations, + violations: [], + transaction: {...basicProps.transaction, errors: undefined, errorFields: undefined}, + }; + + const resultWithViolations = createTransactionPreviewConditionals(functionArgsWithViolations); + // RBR should be the same with or without violations when user is not the owner + expect(resultWithViolations.shouldShowRBR).toBe(shouldShowRBRWithoutViolations); + }); + + it('should show RBR when report has violations even if transaction violations are absent', () => { + const reportID = basicProps.iouReport.reportID || '1'; + const iouReport = { + ...basicProps.iouReport, + reportID, + ownerAccountID: currentUserAccountID, + }; + + const reportViolations: ReportViolations = { + fieldRequired: { + merchant: {}, + }, + } as unknown as ReportViolations; + + const functionArgs = { + ...basicProps, + iouReport, + reportViolations, + violations: [], // No transaction violations + transaction: {...basicProps.transaction, errors: undefined, errorFields: undefined}, + }; + + const result = createTransactionPreviewConditionals(functionArgs); + expect(result.shouldShowRBR).toBeTruthy(); + }); + it('should show description if no merchant is presented and is not scanning', () => { const functionArgs = {...basicProps, transactionDetails: {comment: 'A valid comment', merchant: ''}}; const result = createTransactionPreviewConditionals(functionArgs);