diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 74bc7295273eb..3fee7e6270717 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -11,6 +11,7 @@ import {InteractionManager, View} from 'react-native'; import type {ValueOf} from 'type-fest'; import useActiveAdminPolicies from '@hooks/useActiveAdminPolicies'; import useConfirmModal from '@hooks/useConfirmModal'; +import useConfirmPendingRTERAndProceed from '@hooks/useConfirmPendingRTERAndProceed'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDefaultExpensePolicy from '@hooks/useDefaultExpensePolicy'; import useDeleteTransactions from '@hooks/useDeleteTransactions'; @@ -124,6 +125,7 @@ import { allHavePendingRTERViolation, getChildTransactions, getOriginalTransactionWithSplitInfo, + hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, hasCustomUnitOutOfPolicyViolation as hasCustomUnitOutOfPolicyViolationTransactionUtils, hasDuplicateTransactions, isDistanceRequest, @@ -155,7 +157,7 @@ import { unapproveExpenseReport, } from '@userActions/IOU'; import {setDeleteTransactionNavigateBackUrl} from '@userActions/Report'; -import {markAsCash as markAsCashAction} from '@userActions/Transaction'; +import {markAsCash as markAsCashAction, markPendingRTERTransactionsAsCash} from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -476,6 +478,12 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa () => allHavePendingRTERViolation(transactions, violations, email ?? '', accountID, moneyRequestReport, policy), [transactions, violations, email, accountID, moneyRequestReport, policy], ); + // Check if any transactions have pending RTER violations (for showing the submit confirmation modal) + const hasAnyPendingRTERViolation = useMemo( + () => hasAnyPendingRTERViolationTransactionUtils(transactions, allTransactionViolations, email ?? '', accountID, moneyRequestReport, policy), + [transactions, allTransactionViolations, email, accountID, moneyRequestReport, policy], + ); + // Check if user should see broken connection violation warning. const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactions, moneyRequestReport, policy, violations, email ?? '', accountID); const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(moneyRequestReport?.reportID); @@ -878,6 +886,12 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa ], ); + const handleMarkPendingRTERTransactionsAsCash = useCallback(() => { + markPendingRTERTransactionsAsCash(transactions, allTransactionViolations, reportActions); + }, [transactions, allTransactionViolations, reportActions]); + + const confirmPendingRTERAndProceed = useConfirmPendingRTERAndProceed(hasAnyPendingRTERViolation, handleMarkPendingRTERTransactionsAsCash); + const handleSubmitReport = useCallback( (skipAnimation = false) => { if (!moneyRequestReport || shouldBlockSubmit) { @@ -887,37 +901,40 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa showDWEModal(); return; } - submitReport({ - expenseReport: moneyRequestReport, - policy, - currentUserAccountIDParam: accountID, - currentUserEmailParam: email ?? '', - hasViolations, - isASAPSubmitBetaEnabled, - expenseReportCurrentNextStepDeprecated: nextStep, - userBillingGraceEndPeriods, - amountOwed, - onSubmitted: () => { - if (skipAnimation) { - return; - } - startSubmittingAnimation(); - }, - ownerBillingGraceEndPeriod, - }); - if (currentSearchQueryJSON && !isOffline) { - search({ - searchKey: currentSearchKey, - shouldCalculateTotals, - offset: 0, - queryJSON: currentSearchQueryJSON, - isOffline, - isLoading: !!currentSearchResults?.search?.isLoading, + const doSubmit = () => { + submitReport({ + expenseReport: moneyRequestReport, + policy, + currentUserAccountIDParam: accountID, + currentUserEmailParam: email ?? '', + hasViolations, + isASAPSubmitBetaEnabled, + expenseReportCurrentNextStepDeprecated: nextStep, + userBillingGraceEndPeriods, + amountOwed, + onSubmitted: () => { + if (skipAnimation) { + return; + } + startSubmittingAnimation(); + }, + ownerBillingGraceEndPeriod, }); - } - if (skipAnimation) { - clearSelectedTransactions(true); - } + if (currentSearchQueryJSON && !isOffline) { + search({ + searchKey: currentSearchKey, + shouldCalculateTotals, + offset: 0, + queryJSON: currentSearchQueryJSON, + isOffline, + isLoading: !!currentSearchResults?.search?.isLoading, + }); + } + if (skipAnimation) { + clearSelectedTransactions(true); + } + }; + confirmPendingRTERAndProceed(doSubmit); }, [ moneyRequestReport, @@ -939,6 +956,7 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa shouldCalculateTotals, currentSearchResults?.search?.isLoading, clearSelectedTransactions, + confirmPendingRTERAndProceed, ownerBillingGraceEndPeriod, ], ); @@ -1743,17 +1761,19 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa showDWEModal(); return; } - submitReport({ - expenseReport: moneyRequestReport, - policy, - currentUserAccountIDParam: accountID, - currentUserEmailParam: email ?? '', - hasViolations, - isASAPSubmitBetaEnabled, - expenseReportCurrentNextStepDeprecated: nextStep, - userBillingGraceEndPeriods, - amountOwed, - ownerBillingGraceEndPeriod, + confirmPendingRTERAndProceed(() => { + submitReport({ + expenseReport: moneyRequestReport, + policy, + currentUserAccountIDParam: accountID, + currentUserEmailParam: email ?? '', + hasViolations, + isASAPSubmitBetaEnabled, + expenseReportCurrentNextStepDeprecated: nextStep, + userBillingGraceEndPeriods, + amountOwed, + ownerBillingGraceEndPeriod, + }); }); }, }, diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index 5b359da24e8ea..ee2cba231bf6b 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -28,6 +28,7 @@ import {showContextMenuForReport} from '@components/ShowContextMenuContext'; import StatusBadge from '@components/StatusBadge'; import Text from '@components/Text'; import useConfirmModal from '@hooks/useConfirmModal'; +import useConfirmPendingRTERAndProceed from '@hooks/useConfirmPendingRTERAndProceed'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -78,11 +79,12 @@ import { import shouldAdjustScroll from '@libs/shouldAdjustScroll'; import {startSpan} from '@libs/telemetry/activeSpans'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; -import {hasPendingUI, isManagedCardTransaction, isPending} from '@libs/TransactionUtils'; +import {hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, hasPendingUI, isManagedCardTransaction, isPending} from '@libs/TransactionUtils'; import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import {approveMoneyRequest, canIOUBePaid as canIOUBePaidIOUActions, payInvoice, payMoneyRequest, submitReport} from '@userActions/IOU'; import {openOldDotLink} from '@userActions/Link'; +import {markPendingRTERTransactionsAsCash} from '@userActions/Transaction'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -203,6 +205,11 @@ function MoneyRequestReportPreviewContent({ const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const isDEWBetaEnabled = isBetaEnabled(CONST.BETAS.NEW_DOT_DEW); const hasViolations = hasViolationsReportUtils(iouReport?.reportID, transactionViolations, currentUserAccountID, currentUserEmail); + const hasAnyPendingRTERViolation = useMemo( + () => hasAnyPendingRTERViolationTransactionUtils(transactions, transactionViolations, currentUserEmail, currentUserAccountID, iouReport, policy), + [transactions, transactionViolations, currentUserEmail, currentUserAccountID, iouReport, policy], + ); + const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); const getCanIOUBePaid = useCallback( @@ -260,6 +267,12 @@ function MoneyRequestReportPreviewContent({ const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`); + const handleMarkPendingRTERTransactionsAsCash = useCallback(() => { + markPendingRTERTransactionsAsCash(transactions, transactionViolations, Object.values(reportActions ?? {})); + }, [transactions, transactionViolations, reportActions]); + + const confirmPendingRTERAndProceed = useConfirmPendingRTERAndProceed(hasAnyPendingRTERViolation, handleMarkPendingRTERTransactionsAsCash); + // The submit button should be success green color only if the user is submitter and the policy does not have Scheduled Submit turned on // Or if the report has been reopened or retracted const isWaitingForSubmissionFromCurrentUser = useMemo( @@ -765,18 +778,20 @@ function MoneyRequestReportPreviewContent({ showDEWModal(); return; } - submitReport({ - expenseReport: iouReport, - policy, - currentUserAccountIDParam: currentUserAccountID, - currentUserEmailParam: currentUserEmail, - hasViolations, - isASAPSubmitBetaEnabled, - expenseReportCurrentNextStepDeprecated: iouReportNextStep, - userBillingGraceEndPeriods, - amountOwed, - onSubmitted: startSubmittingAnimation, - ownerBillingGraceEndPeriod, + confirmPendingRTERAndProceed(() => { + submitReport({ + expenseReport: iouReport, + policy, + currentUserAccountIDParam: currentUserAccountID, + currentUserEmailParam: currentUserEmail, + hasViolations, + isASAPSubmitBetaEnabled, + expenseReportCurrentNextStepDeprecated: iouReportNextStep, + userBillingGraceEndPeriods, + amountOwed, + onSubmitted: startSubmittingAnimation, + ownerBillingGraceEndPeriod, + }); }); }} isSubmittingAnimationRunning={isSubmittingAnimationRunning} diff --git a/src/hooks/useConfirmPendingRTERAndProceed.ts b/src/hooks/useConfirmPendingRTERAndProceed.ts new file mode 100644 index 0000000000000..234a154861ccf --- /dev/null +++ b/src/hooks/useConfirmPendingRTERAndProceed.ts @@ -0,0 +1,36 @@ +import {useCallback} from 'react'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; +import useConfirmModal from '@hooks/useConfirmModal'; +import useLocalize from '@hooks/useLocalize'; + +/** + * Hook that returns a callback to confirm pending RTER violations before proceeding with an action. + * If there are pending RTER violations, a confirmation modal is shown asking the user to mark them as cash. + */ +function useConfirmPendingRTERAndProceed(hasAnyPendingRTERViolation: boolean, onMarkAsCash: () => void) { + const {showConfirmModal} = useConfirmModal(); + const {translate} = useLocalize(); + + return useCallback( + (onProceed: () => void) => { + if (!hasAnyPendingRTERViolation) { + onProceed(); + return; + } + showConfirmModal({ + title: translate('iou.pendingMatchSubmitTitle'), + prompt: translate('iou.pendingMatchSubmitDescription'), + confirmText: translate('common.yes'), + cancelText: translate('common.no'), + }).then((result) => { + if (result.action === ModalActions.CONFIRM) { + onMarkAsCash(); + } + onProceed(); + }); + }, + [hasAnyPendingRTERViolation, showConfirmModal, translate, onMarkAsCash], + ); +} + +export default useConfirmPendingRTERAndProceed; diff --git a/src/languages/de.ts b/src/languages/de.ts index 2dab5e5b0d0da..a37b153bcbc70 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1248,6 +1248,8 @@ const translations: TranslationDeepObject = { pendingMatch: 'Ausstehende Zuordnung', pendingMatchWithCreditCardDescription: 'Beleg wartet auf Abgleich mit Kartenumsatz. Als Barzahlung markieren, um abzubrechen.', markAsCash: 'Als Bar markieren', + pendingMatchSubmitTitle: 'Bericht einreichen', + pendingMatchSubmitDescription: 'Einige Ausgaben warten auf die Zuordnung mit einer Kreditkartentransaktion. Möchten Sie sie als Bar markieren?', routePending: 'Routing ausstehend ...', automaticallyEnterExpenseDetails: 'Concierge wird automatisch die Ausgabendetails für Sie eingeben, oder Sie können sie manuell hinzufügen.', receiptScanning: () => ({ diff --git a/src/languages/en.ts b/src/languages/en.ts index 88d0ed3e1f781..d065c3dcf363d 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1296,6 +1296,8 @@ const translations = { pendingMatch: 'Pending match', pendingMatchWithCreditCardDescription: 'Receipt pending match with card transaction. Mark as cash to cancel.', markAsCash: 'Mark as cash', + pendingMatchSubmitTitle: 'Submit report', + pendingMatchSubmitDescription: 'Some expenses are awaiting a match with a credit card transaction. Do you want to mark them as cash?', routePending: 'Route pending...', automaticallyEnterExpenseDetails: 'Concierge will automatically enter the expense details for you, or you can add them manually.', receiptScanning: () => ({ diff --git a/src/languages/es.ts b/src/languages/es.ts index 5114871383ff3..ced31609bc4fc 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1153,6 +1153,8 @@ const translations: TranslationDeepObject = { pendingMatchWithCreditCard: 'Recibo pendiente de adjuntar con la transacción de la tarjeta', pendingMatchWithCreditCardDescription: 'Recibo pendiente de adjuntar con la transacción de la tarjeta. Márcalo como efectivo para cancelar.', markAsCash: 'Marcar como efectivo', + pendingMatchSubmitTitle: 'Enviar informe', + pendingMatchSubmitDescription: 'Algunos gastos están pendientes de coincidencia con una transacción de tarjeta de crédito. ¿Deseas marcarlos como efectivo?', routePending: 'Ruta pendiente...', findExpense: 'Buscar gasto', deletedTransaction: (amount, merchant) => `eliminó un gasto (${amount} para ${merchant})`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index f1200a74fdbd1..342fc1b4c0c1a 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1252,6 +1252,8 @@ const translations: TranslationDeepObject = { pendingMatch: 'Correspondance en attente', pendingMatchWithCreditCardDescription: 'Reçu en attente de rapprochement avec une transaction par carte. Marquez comme paiement en espèces pour annuler.', markAsCash: 'Marquer comme espèces', + pendingMatchSubmitTitle: 'Soumettre le rapport', + pendingMatchSubmitDescription: 'Certaines dépenses sont en attente de rapprochement avec une transaction par carte de crédit. Voulez-vous les marquer comme espèces ?', routePending: 'Acheminement en attente...', automaticallyEnterExpenseDetails: 'Concierge saisira automatiquement les détails de la dépense pour vous, ou vous pouvez les ajouter manuellement.', receiptScanning: () => ({ diff --git a/src/languages/it.ts b/src/languages/it.ts index 37cd717669d47..b8a35c8e590a5 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1247,6 +1247,8 @@ const translations: TranslationDeepObject = { pendingMatch: 'Corrispondenza in sospeso', pendingMatchWithCreditCardDescription: 'Ricevuta in attesa di abbinamento con la transazione della carta. Contrassegna come contante per annullare.', markAsCash: 'Segna come contante', + pendingMatchSubmitTitle: 'Invia report', + pendingMatchSubmitDescription: 'Alcune spese sono in attesa di abbinamento con una transazione della carta di credito. Vuoi segnarle come contante?', routePending: 'Instradamento in sospeso...', automaticallyEnterExpenseDetails: 'Concierge inserirà automaticamente i dettagli della spesa per te, oppure puoi aggiungerli manualmente.', receiptScanning: () => ({ diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 8ea363a221c86..3970828ad7c49 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1230,6 +1230,8 @@ const translations: TranslationDeepObject = { pendingMatch: '保留中の照合', pendingMatchWithCreditCardDescription: 'レシートはカード取引との照合待ちです。現金としてマークしてキャンセルします。', markAsCash: '現金としてマーク', + pendingMatchSubmitTitle: 'レポートを提出', + pendingMatchSubmitDescription: '一部の経費がクレジットカード取引との照合待ちです。現金としてマークしますか?', routePending: 'ルート保留中…', automaticallyEnterExpenseDetails: 'コンシェルジュが自動的に経費の詳細を入力するか、手動で追加することができます。', receiptScanning: () => ({ diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 4978d187e7dc0..15b873eba12c8 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1246,6 +1246,8 @@ const translations: TranslationDeepObject = { pendingMatch: 'Overeenkomst in behandeling', pendingMatchWithCreditCardDescription: 'Bon wordt nog gekoppeld aan kaarttransactie. Markeer als contant om te annuleren.', markAsCash: 'Markeren als contant', + pendingMatchSubmitTitle: 'Rapport indienen', + pendingMatchSubmitDescription: 'Sommige uitgaven wachten op koppeling met een creditcardtransactie. Wilt u ze als contant markeren?', routePending: 'Routeren in behandeling...', automaticallyEnterExpenseDetails: 'Concierge zal automatisch de uitgavendetails voor je invoeren, of je kunt ze handmatig toevoegen.', receiptScanning: () => ({ diff --git a/src/languages/pl.ts b/src/languages/pl.ts index a30d0aab2915f..38f10ba70ea18 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1246,6 +1246,8 @@ const translations: TranslationDeepObject = { pendingMatch: 'Oczekujące dopasowanie', pendingMatchWithCreditCardDescription: 'Oczekuje na dopasowanie paragonu do transakcji kartą. Oznacz jako gotówkę, aby anulować.', markAsCash: 'Oznacz jako gotówkę', + pendingMatchSubmitTitle: 'Wyślij raport', + pendingMatchSubmitDescription: 'Niektóre wydatki oczekują na dopasowanie z transakcją kartą kredytową. Czy chcesz oznaczyć je jako gotówkę?', routePending: 'Trasa w toku…', automaticallyEnterExpenseDetails: 'Concierge automatycznie wprowadzi szczegóły wydatku za Ciebie lub możesz dodać je ręcznie.', receiptScanning: () => ({ diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index b919fb7aac345..1e8c4f4c4559f 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1245,6 +1245,8 @@ const translations: TranslationDeepObject = { pendingMatch: 'Correspondência pendente', pendingMatchWithCreditCardDescription: 'Recibo aguardando correspondência com transação do cartão. Marque como dinheiro para cancelar.', markAsCash: 'Marcar como dinheiro', + pendingMatchSubmitTitle: 'Enviar relatório', + pendingMatchSubmitDescription: 'Algumas despesas estão pendentes de correspondência com uma transação de cartão de crédito. Deseja marcá-las como dinheiro?', routePending: 'Rota pendente...', automaticallyEnterExpenseDetails: 'O Concierge inserirá automaticamente os detalhes da despesa para você, ou você pode adicioná-los manualmente.', receiptScanning: () => ({ diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 048c91e54335a..b03d05b6174b4 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1210,6 +1210,8 @@ const translations: TranslationDeepObject = { pendingMatch: '待匹配', pendingMatchWithCreditCardDescription: '收据正在等待与卡片交易匹配。将其标记为现金以取消。', markAsCash: '标记为现金', + pendingMatchSubmitTitle: '提交报告', + pendingMatchSubmitDescription: '部分费用正在等待与信用卡交易匹配。您要将它们标记为现金吗?', routePending: '路由处理中…', automaticallyEnterExpenseDetails: 'Concierge 将自动为您输入费用详情,或者您可以手动添加。', receiptScanning: () => ({ diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 52a05af23c8ad..62fc8c0823438 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1559,6 +1559,23 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | ); } +/** + * Check if any of the given transactions have a pending RTER violation that has not been dismissed (e.g. via mark-as-cash). + */ +function hasAnyPendingRTERViolation( + transactions: Array>, + allTransactionViolations: OnyxCollection, + currentUserEmail: string, + currentUserAccountID: number, + report: OnyxEntry, + policy: OnyxEntry, +): boolean { + return transactions.some((t) => { + const filteredViolations = getTransactionViolations(t, allTransactionViolations, currentUserEmail, currentUserAccountID, report, policy); + return hasPendingRTERViolation(filteredViolations); + }); +} + /** * Check if there is a custom unit out of policy violation in transactionViolations. */ @@ -2928,6 +2945,7 @@ export { areRequiredFieldsEmpty, hasMissingSmartscanFields, hasPendingRTERViolation, + hasAnyPendingRTERViolation, hasValidModifiedAmount, allHavePendingRTERViolation, hasPendingUI, diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index e07fe1727a4b0..f3bfc8a02f6b9 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -19,7 +19,14 @@ import {buildNextStepNew, buildOptimisticNextStep} from '@libs/NextStepUtils'; import * as NumberUtils from '@libs/NumberUtils'; import {rand64} from '@libs/NumberUtils'; import {hasDependentTags, isPaidGroupPolicy} from '@libs/PolicyUtils'; -import {getAllReportActions, getIOUActionForReportID, getOriginalMessage, getTrackExpenseActionableWhisper, isModifiedExpenseAction} from '@libs/ReportActionsUtils'; +import { + getAllReportActions, + getIOUActionForReportID, + getIOUActionForTransactionID, + getOriginalMessage, + getTrackExpenseActionableWhisper, + isModifiedExpenseAction, +} from '@libs/ReportActionsUtils'; import { buildOptimisticCreatedReportAction, buildOptimisticDismissedViolationReportAction, @@ -34,7 +41,14 @@ import { hasViolations as hasViolationsReportUtils, shouldEnableNegative, } from '@libs/ReportUtils'; -import {isManagedCardTransaction, isOnHold, recalculateUnreportedTransactionDetails, shouldClearConvertedAmount, waypointHasValidAddress} from '@libs/TransactionUtils'; +import { + hasPendingRTERViolation, + isManagedCardTransaction, + isOnHold, + recalculateUnreportedTransactionDetails, + shouldClearConvertedAmount, + waypointHasValidAddress, +} from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -782,6 +796,26 @@ function markAsCash(transactionID: string | undefined, transactionThreadReportID return API.write(WRITE_COMMANDS.MARK_AS_CASH, parameters, onyxData); } +/** + * Marks all transactions that have pending RTER violations as cash. + */ +function markPendingRTERTransactionsAsCash(transactions: Array>, violationsCollection: OnyxCollection, reportActions: ReportAction[]) { + for (const t of transactions) { + if (!t?.transactionID) { + continue; + } + const txViolations = violationsCollection?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t.transactionID}`]; + if (!hasPendingRTERViolation(txViolations)) { + continue; + } + const action = getIOUActionForTransactionID(reportActions, t.transactionID); + const threadReportID = action?.childReportID; + if (threadReportID) { + markAsCash(t.transactionID, threadReportID, txViolations ?? []); + } + } +} + function openDraftDistanceExpense() { const onyxData: OnyxData = { optimisticData: [ @@ -1636,6 +1670,7 @@ export { updateWaypoints, clearError, markAsCash, + markPendingRTERTransactionsAsCash, dismissDuplicateTransactionViolation, generateTransactionID, setReviewDuplicatesKey,