diff --git a/src/components/DotIndicatorMessage.tsx b/src/components/DotIndicatorMessage.tsx index 3765d1e3b168..81297c74f63f 100644 --- a/src/components/DotIndicatorMessage.tsx +++ b/src/components/DotIndicatorMessage.tsx @@ -5,7 +5,9 @@ import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {isReceiptError} from '@libs/ErrorUtils'; import fileDownload from '@libs/fileDownload'; +import type {MaybePhraseKey} from '@libs/Localize'; import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import type {ReceiptError} from '@src/types/onyx/Transaction'; @@ -34,14 +36,6 @@ type DotIndicatorMessageProps = { textStyles?: StyleProp; }; -/** Check if the error includes a receipt. */ -function isReceiptError(message: Localize.MaybePhraseKey | ReceiptError): message is ReceiptError { - if (typeof message === 'string' || Array.isArray(message)) { - return false; - } - return (message?.error ?? '') === CONST.IOU.RECEIPT_ERROR; -} - function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndicatorMessageProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -52,12 +46,12 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica } // Fetch the keys, sort them, and map through each key to get the corresponding message - const sortedMessages = Object.keys(messages) + const sortedMessages: Array = Object.keys(messages) .sort() .map((key) => messages[key]); // Removing duplicates using Set and transforming the result into an array - const uniqueMessages = [...new Set(sortedMessages)].map((message) => (isReceiptError(message) ? message : Localize.translateIfPhraseKey(message))); + const uniqueMessages: Array = [...new Set(sortedMessages)].map((message) => (isReceiptError(message) ? message : Localize.translateIfPhraseKey(message))); const isErrorMessage = type === 'error'; diff --git a/src/components/ReportActionItem/MoneyRequestPreview.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx similarity index 78% rename from src/components/ReportActionItem/MoneyRequestPreview.tsx rename to src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 7577c6b547e4..e2f7314afd73 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -3,9 +3,7 @@ import {truncate} from 'lodash'; import lodashSortBy from 'lodash/sortBy'; import React from 'react'; import {View} from 'react-native'; -import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {GestureResponderEvent} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestSkeletonView from '@components/MoneyRequestSkeletonView'; @@ -13,6 +11,7 @@ import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithFeedback from '@components/Pressable/PressableWithoutFeedback'; import RenderHTML from '@components/RenderHTML'; +import ReportActionItemImages from '@components/ReportActionItem/ReportActionItemImages'; import {showContextMenuForReport} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -24,86 +23,22 @@ import ControlSelection from '@libs/ControlSelection'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as IOUUtils from '@libs/IOUUtils'; +import * as Localize from '@libs/Localize'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import * as PaymentMethods from '@userActions/PaymentMethods'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; -import * as Localize from '@src/libs/Localize'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type * as OnyxTypes from '@src/types/onyx'; import type {IOUMessage} from '@src/types/onyx/OriginalMessage'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; -import ReportActionItemImages from './ReportActionItemImages'; - -type MoneyRequestPreviewOnyxProps = { - /** All of the personal details for everyone */ - personalDetails: OnyxEntry; - - /** Chat report associated with iouReport */ - chatReport: OnyxEntry; - - /** IOU report data object */ - iouReport: OnyxEntry; - - /** Session info for the currently logged in user. */ - session: OnyxEntry; - - /** The transaction attached to the action.message.iouTransactionID */ - transaction: OnyxEntry; - - /** Information about the user accepting the terms for payments */ - walletTerms: OnyxEntry; -}; - -type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & { - /** The active IOUReport, used for Onyx subscription */ - // The iouReportID is used inside withOnyx HOC - // eslint-disable-next-line react/no-unused-prop-types - iouReportID: string; - - /** The associated chatReport */ - chatReportID: string; - - /** The ID of the current report */ - reportID: string; - - /** Callback for the preview pressed */ - onPreviewPressed: (event?: GestureResponderEvent | KeyboardEvent) => void; - - /** All the data of the action, used for showing context menu */ - action: OnyxTypes.ReportAction; - - /** Popover context menu anchor, used for showing context menu */ - contextMenuAnchor?: ContextMenuAnchor; - - /** Callback for updating context menu active state, used for showing context menu */ - checkIfContextMenuActive?: () => void; - - /** Extra styles to pass to View wrapper */ - containerStyles?: StyleProp; - - /** True if this is this IOU is a split instead of a 1:1 request */ - isBillSplit: boolean; - - /** True if the IOU Preview card is hovered */ - isHovered?: boolean; - - /** Whether or not an IOU report contains money requests in a different currency - * that are either created or cancelled offline, and thus haven't been converted to the report's currency yet - */ - shouldShowPendingConversionMessage?: boolean; - - /** Whether a message is a whisper */ - isWhisper?: boolean; -}; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {MoneyRequestPreviewProps} from './types'; -function MoneyRequestPreview({ +function MoneyRequestPreviewContent({ iouReport, isBillSplit, session, @@ -121,6 +56,7 @@ function MoneyRequestPreview({ shouldShowPendingConversionMessage = false, isHovered = false, isWhisper = false, + transactionViolations, }: MoneyRequestPreviewProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -129,10 +65,6 @@ function MoneyRequestPreview({ const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); const parser = new ExpensiMark(); - if (isEmptyObject(iouReport) && !isBillSplit) { - return null; - } - const sessionAccountID = session?.accountID; const managerID = iouReport?.managerID ?? -1; const ownerAccountID = iouReport?.ownerAccountID ?? -1; @@ -153,7 +85,9 @@ function MoneyRequestPreview({ const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const hasReceipt = TransactionUtils.hasReceipt(transaction); const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); + const hasViolations = TransactionUtils.hasViolation(transaction, transactionViolations); const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(transaction); + const shouldShowRBR = hasViolations || hasFieldErrors; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); const isFetchingWaypointsFromServer = TransactionUtils.isFetchingWaypointsFromServer(transaction); const isCardTransaction = TransactionUtils.isCardTransaction(transaction); @@ -213,7 +147,14 @@ function MoneyRequestPreview({ } let message = translate('iou.cash'); - if (ReportUtils.isPaidGroupPolicyExpenseReport(iouReport) && ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID)) { + if (hasViolations && transaction) { + const violations = TransactionUtils.getTransactionViolations(transaction, transactionViolations); + if (violations?.[0]) { + const violationMessage = ViolationsUtils.getViolationTranslation(violations[0], translate); + const isTooLong = violations.filter((v) => v.type === 'violation').length > 1 || violationMessage.length > 15; + message += ` • ${isTooLong ? translate('violations.reviewRequired') : violationMessage}`; + } + } else if (ReportUtils.isPaidGroupPolicyExpenseReport(iouReport) && ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport)) { message += ` • ${translate('iou.approved')}`; } else if (iouReport?.isWaitingOnBankAccount) { message += ` • ${translate('iou.pending')}`; @@ -280,7 +221,7 @@ function MoneyRequestPreview({ {getPreviewHeaderText() + (isSettled && !iouReport?.isCancelledIOU ? ` • ${getSettledMessage()}` : '')} - {!isSettled && hasFieldErrors && ( + {!isSettled && shouldShowRBR && ( ({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - chatReport: { - key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, - }, - iouReport: { - key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, - }, - session: { - key: ONYXKEYS.SESSION, - }, - transaction: { - key: ({action}) => { - const originalMessage = action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? action.originalMessage : undefined; - return `${ONYXKEYS.COLLECTION.TRANSACTION}${originalMessage?.IOUTransactionID ?? 0}`; - }, - }, - walletTerms: { - key: ONYXKEYS.WALLET_TERMS, - }, -})(MoneyRequestPreview); +export default MoneyRequestPreviewContent; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx new file mode 100644 index 000000000000..1c502c53f99e --- /dev/null +++ b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx @@ -0,0 +1,44 @@ +import lodashIsEmpty from 'lodash/isEmpty'; +import React from 'react'; +import {withOnyx} from 'react-native-onyx'; +import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import MoneyRequestPreviewContent from './MoneyRequestPreviewContent'; +import type {MoneyRequestPreviewOnyxProps, MoneyRequestPreviewProps} from './types'; + +function MoneyRequestPreview(props: MoneyRequestPreviewProps) { + // We should not render the component if there is no iouReport and it's not a split. + // Moved outside of the component scope to allow for easier use of hooks in the main component. + // eslint-disable-next-line react/jsx-props-no-spreading + return lodashIsEmpty(props.iouReport) && !props.isBillSplit ? null : ; +} + +MoneyRequestPreview.displayName = 'MoneyRequestPreview'; + +export default withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + chatReport: { + key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, + }, + iouReport: { + key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, + }, + session: { + key: ONYXKEYS.SESSION, + }, + transaction: { + key: ({action}) => { + const isMoneyRequestAction = ReportActionsUtils.isMoneyRequestAction(action); + const transactionID = isMoneyRequestAction ? action?.originalMessage?.IOUTransactionID : 0; + return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; + }, + }, + walletTerms: { + key: ONYXKEYS.WALLET_TERMS, + }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + }, +})(MoneyRequestPreview); diff --git a/src/components/ReportActionItem/MoneyRequestPreview/types.ts b/src/components/ReportActionItem/MoneyRequestPreview/types.ts new file mode 100644 index 000000000000..17dd42b2f794 --- /dev/null +++ b/src/components/ReportActionItem/MoneyRequestPreview/types.ts @@ -0,0 +1,71 @@ +import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import type * as OnyxTypes from '@src/types/onyx'; + +type MoneyRequestPreviewOnyxProps = { + /** All of the personal details for everyone */ + personalDetails: OnyxEntry; + + /** Chat report associated with iouReport */ + chatReport: OnyxEntry; + + /** IOU report data object */ + iouReport: OnyxEntry; + + /** Session info for the currently logged in user. */ + session: OnyxEntry; + + /** The transaction attached to the action.message.iouTransactionID */ + transaction: OnyxEntry; + + /** The transaction violations attached to the action.message.iouTransactionID */ + transactionViolations: OnyxCollection; + + /** Information about the user accepting the terms for payments */ + walletTerms: OnyxEntry; +}; + +type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & { + /** The active IOUReport, used for Onyx subscription */ + // The iouReportID is used inside withOnyx HOC + // eslint-disable-next-line react/no-unused-prop-types + iouReportID: string; + + /** The associated chatReport */ + chatReportID: string; + + /** The ID of the current report */ + reportID: string; + + /** Callback for the preview pressed */ + onPreviewPressed: (event?: GestureResponderEvent | KeyboardEvent) => void; + + /** All the data of the action, used for showing context menu */ + action: OnyxTypes.ReportAction; + + /** Popover context menu anchor, used for showing context menu */ + contextMenuAnchor?: ContextMenuAnchor; + + /** Callback for updating context menu active state, used for showing context menu */ + checkIfContextMenuActive?: () => void; + + /** Extra styles to pass to View wrapper */ + containerStyles?: StyleProp; + + /** True if this is this IOU is a split instead of a 1:1 request */ + isBillSplit: boolean; + + /** True if the IOU Preview card is hovered */ + isHovered?: boolean; + + /** Whether or not an IOU report contains money requests in a different currency + * that are either created or cancelled offline, and thus haven't been converted to the report's currency yet + */ + shouldShowPendingConversionMessage?: boolean; + + /** Whether a message is a whisper */ + isWhisper?: boolean; +}; + +export type {MoneyRequestPreviewProps, MoneyRequestPreviewOnyxProps}; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 7ee3d9894d2d..d3c86698f910 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -135,7 +135,7 @@ function MoneyRequestView({ // Flags for showing categories and tags // transactionCategory can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const shouldShowCategory = isPolicyExpenseChat && (transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + const shouldShowCategory = isPolicyExpenseChat && (transactionCategory || OptionsListUtils.hasEnabledOptions(policyCategories ?? {})); // transactionTag can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)); @@ -248,7 +248,6 @@ function MoneyRequestView({ if (!transaction?.transactionID) { return; } - Transaction.clearError(transaction.transactionID); }} > diff --git a/src/languages/en.ts b/src/languages/en.ts index 07b8e6df850e..82aedf16aac5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2305,7 +2305,7 @@ export default { maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Date older than ${maxAge} days`, missingCategory: 'Missing category', missingComment: 'Description required for selected category', - missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Missing ${tagName ?? 'tag'}`, + missingTag: ({tagName}: ViolationsMissingTagParams) => `Missing ${tagName ?? 'tag'}`, modifiedAmount: 'Amount greater than scanned receipt', modifiedDate: 'Date differs from scanned receipt', nonExpensiworksExpense: 'Non-Expensiworks expense', @@ -2315,8 +2315,8 @@ export default { overLimitAttendee: ({formattedLimit}: ViolationsOverLimitParams) => `Amount over ${formattedLimit}/person limit`, perDayLimit: ({formattedLimit}: ViolationsPerDayLimitParams) => `Amount over daily ${formattedLimit}/person category limit`, receiptNotSmartScanned: 'Receipt not verified. Please confirm accuracy.', - receiptRequired: ({formattedLimit, category}: ViolationsReceiptRequiredParams = {}) => - `Receipt required${formattedLimit ? ` over ${formattedLimit}${category ? ' category limit' : ''}` : ''}`, + receiptRequired: (params: ViolationsReceiptRequiredParams) => `Receipt required${params ? ` over ${params.formattedLimit}${params.category ? ' category limit' : ''}` : ''}`, + reviewRequired: 'Review required', rter: ({brokenBankConnection, email, isAdmin, isTransactionOlderThan7Days, member}: ViolationsRterParams) => { if (brokenBankConnection) { return isAdmin diff --git a/src/languages/es.ts b/src/languages/es.ts index 03c711517e49..466ff1298200 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2794,7 +2794,7 @@ export default { maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Fecha de más de ${maxAge} días`, missingCategory: 'Falta categoría', missingComment: 'Descripción obligatoria para la categoría seleccionada', - missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Falta ${tagName ?? 'etiqueta'}`, + missingTag: ({tagName}: ViolationsMissingTagParams) => `Falta ${tagName ?? 'etiqueta'}`, modifiedAmount: 'Importe superior al del recibo escaneado', modifiedDate: 'Fecha difiere del recibo escaneado', nonExpensiworksExpense: 'Gasto no proviene de Expensiworks', @@ -2805,8 +2805,9 @@ export default { overLimitAttendee: ({formattedLimit}: ViolationsOverLimitParams) => `Importe supera el límite${formattedLimit ? ` de ${formattedLimit}/persona` : ''}`, perDayLimit: ({formattedLimit}: ViolationsPerDayLimitParams) => `Importe supera el límite diario de la categoría${formattedLimit ? ` de ${formattedLimit}/persona` : ''}`, receiptNotSmartScanned: 'Recibo no verificado. Por favor, confirma su exactitud', - receiptRequired: ({formattedLimit, category}: ViolationsReceiptRequiredParams = {}) => - `Recibo obligatorio${formattedLimit ? ` para importes sobre ${category ? 'el limite de la categoría de ' : ''}${formattedLimit}` : ''}`, + receiptRequired: (params: ViolationsReceiptRequiredParams) => + `Recibo obligatorio${params ? ` para importes sobre${params.formattedLimit ? ` ${params.formattedLimit}` : ''}${params.category ? ' el límite de la categoría' : ''}` : ''}`, + reviewRequired: 'Revisión requerida', rter: ({brokenBankConnection, isAdmin, email, isTransactionOlderThan7Days, member}: ViolationsRterParams) => { if (brokenBankConnection) { return isAdmin diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts index 7db0cd4c3eb0..20313ee8912d 100644 --- a/src/libs/ErrorUtils.ts +++ b/src/libs/ErrorUtils.ts @@ -3,6 +3,7 @@ import CONST from '@src/CONST'; import type {TranslationFlatObject, TranslationPaths} from '@src/languages/types'; import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; import type Response from '@src/types/onyx/Response'; +import type {ReceiptError} from '@src/types/onyx/Transaction'; import DateUtils from './DateUtils'; import * as Localize from './Localize'; @@ -150,15 +151,32 @@ function addErrorMessage(errors: Errors, inputID? } } +/** + * Check if the error includes a receipt. + */ +function isReceiptError(message: unknown): message is ReceiptError { + if (typeof message === 'string') { + return false; + } + if (Array.isArray(message)) { + return false; + } + if (Object.keys(message as Record).length === 0) { + return false; + } + return ((message as Record)?.error ?? '') === CONST.IOU.RECEIPT_ERROR; +} + export { + addErrorMessage, getAuthenticateErrorMessage, - getMicroSecondOnyxError, - getMicroSecondOnyxErrorObject, - getLatestErrorMessage, - getLatestErrorField, getEarliestErrorField, getErrorMessageWithTranslationData, getErrorsWithTranslationData, - addErrorMessage, + getLatestErrorField, + getLatestErrorMessage, getLatestErrorMessageField, + getMicroSecondOnyxError, + getMicroSecondOnyxErrorObject, + isReceiptError, }; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 45b547e338ae..d6f718da2b2c 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -17,7 +17,6 @@ import type { PersonalDetailsList, Policy, PolicyCategories, - PolicyCategory, PolicyTag, PolicyTagList, Report, @@ -796,8 +795,8 @@ function getEnabledCategoriesCount(options: PolicyCategories): number { /** * Verifies that there is at least one enabled option */ -function hasEnabledOptions(options: PolicyCategory[] | PolicyTag[]): boolean { - return options.some((option) => option.enabled); +function hasEnabledOptions(options: PolicyCategories | PolicyTag[]): boolean { + return Object.values(options).some((option) => option.enabled); } /** diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f9e2cd2220b3..eeebbdf7dbdb 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -6,7 +6,7 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ActionName, ChangeLog, OriginalMessageActionableMentionWhisper, OriginalMessageReimbursementDequeued} from '@src/types/onyx/OriginalMessage'; +import type {ActionName, ChangeLog, OriginalMessageActionableMentionWhisper, OriginalMessageIOU, OriginalMessageReimbursementDequeued} from '@src/types/onyx/OriginalMessage'; import type Report from '@src/types/onyx/Report'; import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; @@ -114,7 +114,7 @@ function isPendingRemove(reportAction: OnyxEntry | EmptyObject): b return reportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE; } -function isMoneyRequestAction(reportAction: OnyxEntry): boolean { +function isMoneyRequestAction(reportAction: OnyxEntry): reportAction is ReportAction & OriginalMessageIOU { return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 338b010afb04..453de7219315 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -721,10 +721,12 @@ function hasParticipantInArray(report: Report, policyMemberAccountIDs: number[]) /** * Whether the Money Request report is settled */ -function isSettled(reportID: string | undefined): boolean { - if (!allReports) { +function isSettled(reportOrID: Report | OnyxEntry | string | undefined): boolean { + if (!allReports || !reportOrID) { return false; } + const reportID = typeof reportOrID === 'string' ? reportOrID : reportOrID?.reportID; + const report: Report | EmptyObject = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; if (isEmptyObject(report) || report.isWaitingOnBankAccount) { return false; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 65013fcf0afb..d3eafc6554db 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -550,10 +550,23 @@ function isOnHold(transaction: OnyxEntry): boolean { /** * Checks if any violations for the provided transaction are of type 'violation' */ -function hasViolation(transactionID: string, transactionViolations: OnyxCollection): boolean { +function hasViolation(transactionOrID: Transaction | OnyxEntry | string, transactionViolations: OnyxCollection): boolean { + if (!transactionOrID) { + return false; + } + const transactionID = typeof transactionOrID === 'string' ? transactionOrID : transactionOrID.transactionID; return Boolean(transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'violation')); } +function getTransactionViolations(transactionOrID: OnyxEntry | string, transactionViolations: OnyxCollection): TransactionViolation[] | null { + if (!transactionOrID) { + return null; + } + const transactionID = typeof transactionOrID === 'string' ? transactionOrID : transactionOrID.transactionID; + + return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; +} + /** * this is the formulae to calculate tax */ @@ -592,6 +605,7 @@ export { getCategory, getBillable, getTag, + getTransactionViolations, getLinkedTransaction, getAllReportTransactions, hasReceipt, diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index a7f15d948b61..a1cd001badee 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -138,11 +138,11 @@ const ViolationsUtils = { isTransactionOlderThan7Days = false, member, category, - rejectedBy, - rejectReason, + rejectedBy = '', + rejectReason = '', formattedLimit, - surcharge, - invoiceMarkup, + surcharge = 0, + invoiceMarkup = 0, maxAge = 0, tagName, taxName, @@ -152,12 +152,10 @@ const ViolationsUtils = { case 'allTagLevelsRequired': return translate('violations.allTagLevelsRequired'); case 'autoReportedRejectedExpense': - return rejectReason && rejectedBy - ? translate('violations.autoReportedRejectedExpense', { - rejectedBy, - rejectReason, - }) - : ''; + return translate('violations.autoReportedRejectedExpense', { + rejectedBy, + rejectReason, + }); case 'billableExpense': return translate('violations.billableExpense'); case 'cashExpenseWithNoReceipt': diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index c3499b96fcb9..ee6a7f2d0b4c 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -164,7 +164,7 @@ type Transaction = { taxAmount?: number; /** Pending fields for the transaction */ - pendingFields?: Partial<{[K in TransactionPendingFieldsKey]: ValueOf}>; + pendingFields?: Partial<{[K in keyof Transaction | keyof Comment]: ValueOf}>; /** Card Transactions */