From 4c2fcd1fc3e2df35b50fe47aadab742461a753e7 Mon Sep 17 00:00:00 2001 From: "huult (via MelvinBot)" Date: Fri, 13 Mar 2026 03:06:59 +0000 Subject: [PATCH 01/11] Improve RTER messaging and add submit confirmation for pending card matches - Update movedTransactionFrom translation to include 'pending a match with a credit card transaction' for clearer auto-move messaging - Add confirmation modal before submitting reports with RTER-pending expenses - Both 'Yes' (mark as cash + submit) and 'No' (just submit) paths proceed to submit - Consistent behavior across MoneyReportHeader and MoneyRequestReportPreviewContent Co-authored-by: huult --- src/components/MoneyReportHeader.tsx | 106 ++++++++++++++++-- .../MoneyRequestReportPreviewContent.tsx | 54 ++++++++- src/languages/de.ts | 5 +- src/languages/en.ts | 5 +- src/languages/es.ts | 5 +- src/languages/fr.ts | 5 +- src/languages/it.ts | 5 +- src/languages/ja.ts | 5 +- src/languages/nl.ts | 5 +- src/languages/pl.ts | 5 +- src/languages/pt-BR.ts | 5 +- src/languages/zh-hans.ts | 4 +- 12 files changed, 186 insertions(+), 23 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 26779cf6ce820..43e50eaa18e71 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -63,7 +63,15 @@ import { import type {KYCFlowEvent, TriggerKYCFlow} from '@libs/PaymentUtils'; import {selectPaymentType} from '@libs/PaymentUtils'; import {getConnectedIntegration, getValidConnectedIntegration, hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; -import {getIOUActionForReportID, getOriginalMessage, getReportAction, hasPendingDEWApprove, hasPendingDEWSubmit, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import { + getIOUActionForReportID, + getIOUActionForTransactionID, + getOriginalMessage, + getReportAction, + hasPendingDEWApprove, + hasPendingDEWSubmit, + isMoneyRequestAction, +} from '@libs/ReportActionsUtils'; import {getAllExpensesToHoldIfApplicable, getReportPrimaryAction, isMarkAsResolvedAction} from '@libs/ReportPrimaryActionUtils'; import {getSecondaryExportReportActions, getSecondaryReportActions} from '@libs/ReportSecondaryActionUtils'; import { @@ -105,6 +113,7 @@ import { getOriginalTransactionWithSplitInfo, hasCustomUnitOutOfPolicyViolation as hasCustomUnitOutOfPolicyViolationTransactionUtils, hasDuplicateTransactions, + hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, isDistanceRequest, isDuplicate, isExpensifyCardTransaction, @@ -445,6 +454,16 @@ function MoneyReportHeader({ () => 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( + () => + transactions.some((t) => { + const txViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t.transactionID}`]; + return hasPendingRTERViolationTransactionUtils(txViolations); + }), + [transactions, allTransactionViolations], + ); + // Check if user should see broken connection violation warning. const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactions, moneyRequestReport, policy, violations, email ?? '', accountID); const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(moneyRequestReport?.reportID); @@ -775,6 +794,20 @@ function MoneyReportHeader({ markAsCashAction(iouTransactionID, reportID, transactionViolations); }, [iouTransactionID, requestParentReportAction, transactionThreadReport?.reportID, transactionViolations]); + const markPendingRTERTransactionsAsCash = useCallback(() => { + for (const t of transactions) { + const txViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t.transactionID}`]; + if (!hasPendingRTERViolationTransactionUtils(txViolations)) { + continue; + } + const action = getIOUActionForTransactionID(reportActions, t.transactionID); + const threadReportID = action?.childReportID; + if (threadReportID) { + markAsCashAction(t.transactionID, threadReportID, txViolations ?? []); + } + } + }, [transactions, allTransactionViolations, reportActions]); + const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); const targetPolicyTags = defaultExpensePolicy ? (allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${defaultExpensePolicy.id}`] ?? {}) : {}; @@ -1158,6 +1191,40 @@ function MoneyReportHeader({ beginExportWithTemplate, ]); + const proceedWithSubmit = useCallback(() => { + if (!moneyRequestReport) { + return; + } + startSubmittingAnimation(); + submitReport(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, userBillingGraceEndPeriods, amountOwed); + if (currentSearchQueryJSON && !isOffline) { + search({ + searchKey: currentSearchKey, + shouldCalculateTotals, + offset: 0, + queryJSON: currentSearchQueryJSON, + isOffline, + isLoading: !!currentSearchResults?.search?.isLoading, + }); + } + }, [ + moneyRequestReport, + policy, + accountID, + email, + hasViolations, + isASAPSubmitBetaEnabled, + nextStep, + userBillingGraceEndPeriods, + amountOwed, + currentSearchQueryJSON, + isOffline, + currentSearchKey, + shouldCalculateTotals, + currentSearchResults?.search?.isLoading, + startSubmittingAnimation, + ]); + const primaryActionsImplementation = { [CONST.REPORT.PRIMARY_ACTIONS.SUBMIT]: ( { + if (result.action === ModalActions.CONFIRM) { + markPendingRTERTransactionsAsCash(); + } + proceedWithSubmit(); }); + return; } + proceedWithSubmit(); }} isSubmittingAnimationRunning={isSubmittingAnimationRunning} onAnimationFinish={stopAnimation} @@ -1448,6 +1518,20 @@ function MoneyReportHeader({ showDWEModal(); return; } + if (hasAnyPendingRTERViolation) { + showConfirmModal({ + title: translate('iou.pendingMatchSubmitTitle'), + prompt: translate('iou.pendingMatchSubmitDescription'), + confirmText: translate('common.yes'), + cancelText: translate('common.no'), + }).then((result) => { + if (result.action === ModalActions.CONFIRM) { + markPendingRTERTransactionsAsCash(); + } + submitReport(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, userBillingGraceEndPeriods, amountOwed); + }); + return; + } submitReport(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, userBillingGraceEndPeriods, amountOwed); }, }, diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index 0a6f33c5630c6..dedac1063f129 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -48,7 +48,7 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import {getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getConnectedIntegration, hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; -import {hasPendingDEWSubmit} from '@libs/ReportActionsUtils'; +import {getIOUActionForTransactionID, hasPendingDEWSubmit} from '@libs/ReportActionsUtils'; import {getInvoicePayerName} from '@libs/ReportNameUtils'; import getReportPreviewAction from '@libs/ReportPreviewActionUtils'; import { @@ -79,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 {hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, 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 {markAsCash as markAsCashAction} from '@userActions/Transaction'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -203,6 +204,15 @@ 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( + () => + transactions.some((t) => { + const txViolations = transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t.transactionID}`]; + return hasPendingRTERViolationTransactionUtils(txViolations); + }), + [transactions, transactionViolations], + ); + const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); const getCanIOUBePaid = useCallback( @@ -260,6 +270,21 @@ function MoneyRequestReportPreviewContent({ const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`); + const markPendingRTERTransactionsAsCash = useCallback(() => { + const reportActionsArray = Object.values(reportActions ?? {}); + for (const t of transactions) { + const txViolations = transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t.transactionID}`]; + if (!hasPendingRTERViolationTransactionUtils(txViolations)) { + continue; + } + const iouAction = getIOUActionForTransactionID(reportActionsArray, t.transactionID); + const threadReportID = iouAction?.childReportID; + if (threadReportID) { + markAsCashAction(t.transactionID, threadReportID, txViolations ?? []); + } + } + }, [transactions, transactionViolations, reportActions]); + // 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( @@ -753,6 +778,31 @@ function MoneyRequestReportPreviewContent({ showDEWModal(); return; } + if (hasAnyPendingRTERViolation) { + showConfirmModal({ + title: translate('iou.pendingMatchSubmitTitle'), + prompt: translate('iou.pendingMatchSubmitDescription'), + confirmText: translate('common.yes'), + cancelText: translate('common.no'), + }).then((result) => { + if (result.action === ModalActions.CONFIRM) { + markPendingRTERTransactionsAsCash(); + } + startSubmittingAnimation(); + submitReport( + iouReport, + policy, + currentUserAccountID, + currentUserEmail, + hasViolations, + isASAPSubmitBetaEnabled, + iouReportNextStep, + userBillingGraceEndPeriods, + amountOwed, + ); + }); + return; + } startSubmittingAnimation(); submitReport( iouReport, diff --git a/src/languages/de.ts b/src/languages/de.ts index ac5090cf337af..1e481a1e3ae34 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1263,7 +1263,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `hat eine Ausgabe gelöscht (${amount} für ${merchant})`, movedFromReport: (reportName: string) => `hat eine Ausgabe verschoben${reportName ? `von ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `hat diese Ausgabe verschoben${reportName ? `zu ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => `hat diese Ausgabe verschoben${reportName ? `von ${reportName}` : ''}`, + movedTransactionFrom: (reportUrl: string, reportName?: string) => + `hat diese Ausgabe verschoben${reportName ? `von ${reportName}` : ''} ausstehende Zuordnung mit einer Kreditkartentransaktion`, unreportedTransaction: (reportUrl: string) => `hat diese Ausgabe in deinen persönlichen Bereich verschoben`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { @@ -1275,6 +1276,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 2f7e97db1d289..619934869275f 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1293,7 +1293,8 @@ const translations = { deletedTransaction: (amount: string, merchant: string) => `deleted an expense (${amount} for ${merchant})`, movedFromReport: (reportName: string) => `moved an expense${reportName ? ` from ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `moved this expense${reportName ? ` to ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => `moved this expense${reportName ? ` from ${reportName}` : ''}`, + movedTransactionFrom: (reportUrl: string, reportName?: string) => + `moved this expense${reportName ? ` from ${reportName}` : ''} pending a match with a credit card transaction`, unreportedTransaction: (reportUrl: string) => `moved this expense to your personal space`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { @@ -1305,6 +1306,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 be67749dfcd57..88628039e4479 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1131,12 +1131,15 @@ 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})`, movedFromReport: (reportName) => `movió un gasto${reportName ? ` desde ${reportName}` : ''}`, movedTransactionTo: (reportUrl, reportName) => `movió este gasto${reportName ? ` a ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl, reportName) => `movió este gasto${reportName ? ` desde ${reportName}` : ''}`, + movedTransactionFrom: (reportUrl, reportName) => + `movió este gasto${reportName ? ` desde ${reportName}` : ''} pendiente de coincidencia con una transacción de tarjeta de crédito`, unreportedTransaction: (reportUrl) => `movió este gasto a tu espacio personal`, movedAction: (shouldHideMovedReportUrl, movedReportUrl, newParentReportUrl, toPolicyName) => { if (shouldHideMovedReportUrl) { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 3a34c0c1947c1..2afc083f0807f 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1267,7 +1267,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `a supprimé une dépense (${amount} pour ${merchant})`, movedFromReport: (reportName: string) => `a déplacé une dépense${reportName ? `de ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `a déplacé cette dépense${reportName ? `vers ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => `a déplacé cette dépense${reportName ? `depuis ${reportName}` : ''}`, + movedTransactionFrom: (reportUrl: string, reportName?: string) => + `a déplacé cette dépense${reportName ? `depuis ${reportName}` : ''} en attente de rapprochement avec une transaction par carte de crédit`, unreportedTransaction: (reportUrl: string) => `a déplacé cette dépense vers votre espace personnel`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { @@ -1279,6 +1280,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 e65e7db28213a..f454ab3bf54b2 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1261,7 +1261,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `ha eliminato una spesa (${amount} per ${merchant})`, movedFromReport: (reportName: string) => `ha spostato una spesa${reportName ? `da ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `ha spostato questa spesa${reportName ? `a ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => `ha spostato questa spesa${reportName ? `da ${reportName}` : ''}`, + movedTransactionFrom: (reportUrl: string, reportName?: string) => + `ha spostato questa spesa${reportName ? `da ${reportName}` : ''} in attesa di abbinamento con una transazione della carta di credito`, unreportedTransaction: (reportUrl: string) => `ha spostato questa spesa nel tuo spazio personale`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { @@ -1273,6 +1274,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 9ab90d2d8dd3d..d213f8ed9a7fb 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1253,7 +1253,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `経費を削除しました(${merchant} に ${amount})`, movedFromReport: (reportName: string) => `経費${reportName ? `${reportName} から` : ''}を移動しました`, movedTransactionTo: (reportUrl: string, reportName?: string) => `この経費を移動しました${reportName ? `${reportName}へ` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => `この経費を移動しました${reportName ? `${reportName} から` : ''}`, + movedTransactionFrom: (reportUrl: string, reportName?: string) => + `この経費を移動しました${reportName ? `${reportName} から` : ''}(クレジットカード取引との照合待ち)`, unreportedTransaction: (reportUrl: string) => `この経費をあなたの個人スペースに移動しました`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { @@ -1265,6 +1266,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 1ade218c60dfc..e67847697522f 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1260,7 +1260,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `heeft een uitgave verwijderd (${amount} voor ${merchant})`, movedFromReport: (reportName: string) => `heeft een uitgave verplaatst${reportName ? `van ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `heeft deze uitgave verplaatst${reportName ? `naar ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => `heeft deze uitgave verplaatst${reportName ? `van ${reportName}` : ''}`, + movedTransactionFrom: (reportUrl: string, reportName?: string) => + `heeft deze uitgave verplaatst${reportName ? `van ${reportName}` : ''} in afwachting van koppeling met een creditcardtransactie`, unreportedTransaction: (reportUrl: string) => `heeft deze uitgave verplaatst naar je persoonlijke ruimte`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { @@ -1272,6 +1273,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 dea6dcd3a6ea8..f0e54d59849eb 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1260,7 +1260,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `usunął(-ę) wydatek (${amount} dla ${merchant})`, movedFromReport: (reportName: string) => `przeniesiono wydatek${reportName ? `z ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `przeniósł ten wydatek${reportName ? `do ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => `przeniósł(-ę) ten wydatek${reportName ? `z ${reportName}` : ''}`, + movedTransactionFrom: (reportUrl: string, reportName?: string) => + `przeniósł(-ę) ten wydatek${reportName ? `z ${reportName}` : ''} w oczekiwaniu na dopasowanie z transakcją kartą kredytową`, unreportedTransaction: (reportUrl: string) => `przeniósł ten wydatek do Twojej przestrzeni osobistej`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { @@ -1272,6 +1273,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 be11c98aa0865..894dd550b08b1 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1259,7 +1259,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `excluiu uma despesa (${amount} em ${merchant})`, movedFromReport: (reportName: string) => `moveu uma despesa${reportName ? `de ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `moveu esta despesa${reportName ? `para ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => `moveu esta despesa${reportName ? `de ${reportName}` : ''}`, + movedTransactionFrom: (reportUrl: string, reportName?: string) => + `moveu esta despesa${reportName ? `de ${reportName}` : ''} pendente de correspondência com uma transação de cartão de crédito`, unreportedTransaction: (reportUrl: string) => `moveu esta despesa para o seu espaço pessoal`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { @@ -1271,6 +1272,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 f87e2bac47587..5037a736474e4 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1236,7 +1236,7 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `已删除一笔报销(${merchant} 的 ${amount})`, movedFromReport: (reportName: string) => `已移动一笔报销${reportName ? `来自${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `到 ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `来自 ${reportName}` : ''}`, + movedTransactionFrom: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `来自 ${reportName}` : ''},等待与信用卡交易匹配`, unreportedTransaction: (reportUrl: string) => `已将此报销移动到你的个人空间`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { @@ -1248,6 +1248,8 @@ const translations: TranslationDeepObject = { pendingMatch: '待匹配', pendingMatchWithCreditCardDescription: '收据正在等待与卡片交易匹配。将其标记为现金以取消。', markAsCash: '标记为现金', + pendingMatchSubmitTitle: '提交报告', + pendingMatchSubmitDescription: '部分费用正在等待与信用卡交易匹配。您要将它们标记为现金吗?', routePending: '路由处理中…', automaticallyEnterExpenseDetails: 'Concierge 将自动为您输入费用详情,或者您可以手动添加。', receiptScanning: () => ({ From 691c3062c16367d55aa3c50ae26fa69afa67b21f Mon Sep 17 00:00:00 2001 From: "huult (via MelvinBot)" Date: Fri, 13 Mar 2026 03:41:42 +0000 Subject: [PATCH 02/11] Refactor: extract hasAnyPendingRTERViolation utility function Move the duplicated hasAnyPendingRTERViolation logic from both MoneyReportHeader and MoneyRequestReportPreviewContent into a reusable utility function in TransactionUtils. Co-authored-by: huult --- src/components/MoneyReportHeader.tsx | 10 ++-------- .../MoneyRequestReportPreviewContent.tsx | 11 ++--------- src/libs/TransactionUtils/index.ts | 11 +++++++++++ 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 43e50eaa18e71..19094f4821041 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -113,6 +113,7 @@ import { getOriginalTransactionWithSplitInfo, hasCustomUnitOutOfPolicyViolation as hasCustomUnitOutOfPolicyViolationTransactionUtils, hasDuplicateTransactions, + hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, isDistanceRequest, isDuplicate, @@ -455,14 +456,7 @@ function MoneyReportHeader({ [transactions, violations, email, accountID, moneyRequestReport, policy], ); // Check if any transactions have pending RTER violations (for showing the submit confirmation modal) - const hasAnyPendingRTERViolation = useMemo( - () => - transactions.some((t) => { - const txViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t.transactionID}`]; - return hasPendingRTERViolationTransactionUtils(txViolations); - }), - [transactions, allTransactionViolations], - ); + const hasAnyPendingRTERViolation = useMemo(() => hasAnyPendingRTERViolationTransactionUtils(transactions, allTransactionViolations), [transactions, allTransactionViolations]); // Check if user should see broken connection violation warning. const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactions, moneyRequestReport, policy, violations, email ?? '', accountID); diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index dedac1063f129..1235ae66e1919 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -79,7 +79,7 @@ import { import shouldAdjustScroll from '@libs/shouldAdjustScroll'; import {startSpan} from '@libs/telemetry/activeSpans'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; -import {hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, hasPendingUI, isManagedCardTransaction, isPending} from '@libs/TransactionUtils'; +import {hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, 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'; @@ -204,14 +204,7 @@ 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( - () => - transactions.some((t) => { - const txViolations = transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t.transactionID}`]; - return hasPendingRTERViolationTransactionUtils(txViolations); - }), - [transactions, transactionViolations], - ); + const hasAnyPendingRTERViolation = useMemo(() => hasAnyPendingRTERViolationTransactionUtils(transactions, transactionViolations), [transactions, transactionViolations]); const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index f5712fd337553..8d19c07db41f1 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1541,6 +1541,16 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | ); } +/** + * Check if any of the given transactions have a pending RTER violation. + */ +function hasAnyPendingRTERViolation(transactions: OnyxEntry[], allTransactionViolations: OnyxCollection): boolean { + return transactions.some((t) => { + const txViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t?.transactionID}`]; + return hasPendingRTERViolation(txViolations); + }); +} + /** * Check if there is a custom unit out of policy violation in transactionViolations. */ @@ -2910,6 +2920,7 @@ export { areRequiredFieldsEmpty, hasMissingSmartscanFields, hasPendingRTERViolation, + hasAnyPendingRTERViolation, hasValidModifiedAmount, allHavePendingRTERViolation, hasPendingUI, From ee6d5bbe5dc451cdc79ee0bec99542233de6caa8 Mon Sep 17 00:00:00 2001 From: "huult (via MelvinBot)" Date: Fri, 13 Mar 2026 03:48:16 +0000 Subject: [PATCH 03/11] Fix ESLint array-type error and Prettier formatting Use Array> instead of OnyxEntry[] to satisfy @typescript-eslint/array-type rule, and fix Prettier formatting in MoneyReportHeader and MoneyRequestReportPreviewContent. Co-authored-by: huult --- src/components/MoneyReportHeader.tsx | 2 +- .../MoneyRequestReportPreviewContent.tsx | 8 +++++++- src/libs/TransactionUtils/index.ts | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 19094f4821041..61d340c238b90 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -111,9 +111,9 @@ import { allHavePendingRTERViolation, getChildTransactions, getOriginalTransactionWithSplitInfo, + hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, hasCustomUnitOutOfPolicyViolation as hasCustomUnitOutOfPolicyViolationTransactionUtils, hasDuplicateTransactions, - hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, isDistanceRequest, isDuplicate, diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index 1235ae66e1919..a8cbd6cd0fa62 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -79,7 +79,13 @@ import { import shouldAdjustScroll from '@libs/shouldAdjustScroll'; import {startSpan} from '@libs/telemetry/activeSpans'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; -import {hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, hasPendingUI, isManagedCardTransaction, isPending} from '@libs/TransactionUtils'; +import { + hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, + hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, + 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'; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 8d19c07db41f1..03397eb8cdcbd 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1544,7 +1544,7 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | /** * Check if any of the given transactions have a pending RTER violation. */ -function hasAnyPendingRTERViolation(transactions: OnyxEntry[], allTransactionViolations: OnyxCollection): boolean { +function hasAnyPendingRTERViolation(transactions: Array>, allTransactionViolations: OnyxCollection): boolean { return transactions.some((t) => { const txViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t?.transactionID}`]; return hasPendingRTERViolation(txViolations); From e7fa4c94d0a26432669b084c3b54c96cfb6e0033 Mon Sep 17 00:00:00 2001 From: "huult (via MelvinBot)" Date: Fri, 13 Mar 2026 04:12:09 +0000 Subject: [PATCH 04/11] Fix: conditionally show pending match message in moved transaction Instead of unconditionally modifying movedTransactionFrom, add a new movedTransactionFromPendingMatch translation key and check the reasoning field in getMovedTransactionMessage() to use it only when the server indicates a pending card match. Co-authored-by: huult --- src/CONST/index.ts | 3 +++ src/languages/de.ts | 3 ++- src/languages/en.ts | 3 ++- src/languages/es.ts | 3 ++- src/languages/fr.ts | 3 ++- src/languages/it.ts | 3 ++- src/languages/ja.ts | 3 ++- src/languages/nl.ts | 3 ++- src/languages/pl.ts | 3 ++- src/languages/pt-BR.ts | 3 ++- src/languages/zh-hans.ts | 3 ++- src/libs/ReportUtils.ts | 5 ++++- 12 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index aa699de640f12..5be26b02f6e3e 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6287,6 +6287,9 @@ const CONST = { BROKEN_CARD_CONNECTION_530: 'brokenCardConnection530', SEVEN_DAY_HOLD: 'sevenDayHold', }, + MOVED_TRANSACTION_REASONS: { + PENDING_CARD_MATCH: 'pendingCardMatch', + }, REVIEW_DUPLICATES_ORDER: ['merchant', 'category', 'tag', 'description', 'taxCode', 'billable', 'reimbursable'], REPORT_VIOLATIONS: { diff --git a/src/languages/de.ts b/src/languages/de.ts index 1e481a1e3ae34..8c98c434c3b30 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1263,7 +1263,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `hat eine Ausgabe gelöscht (${amount} für ${merchant})`, movedFromReport: (reportName: string) => `hat eine Ausgabe verschoben${reportName ? `von ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `hat diese Ausgabe verschoben${reportName ? `zu ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => + movedTransactionFrom: (reportUrl: string, reportName?: string) => `hat diese Ausgabe verschoben${reportName ? `von ${reportName}` : ''}`, + movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => `hat diese Ausgabe verschoben${reportName ? `von ${reportName}` : ''} ausstehende Zuordnung mit einer Kreditkartentransaktion`, unreportedTransaction: (reportUrl: string) => `hat diese Ausgabe in deinen persönlichen Bereich verschoben`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { diff --git a/src/languages/en.ts b/src/languages/en.ts index 619934869275f..304822e2a61e2 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1293,7 +1293,8 @@ const translations = { deletedTransaction: (amount: string, merchant: string) => `deleted an expense (${amount} for ${merchant})`, movedFromReport: (reportName: string) => `moved an expense${reportName ? ` from ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `moved this expense${reportName ? ` to ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => + movedTransactionFrom: (reportUrl: string, reportName?: string) => `moved this expense${reportName ? ` from ${reportName}` : ''}`, + movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => `moved this expense${reportName ? ` from ${reportName}` : ''} pending a match with a credit card transaction`, unreportedTransaction: (reportUrl: string) => `moved this expense to your personal space`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { diff --git a/src/languages/es.ts b/src/languages/es.ts index 88628039e4479..1644206ac8b08 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1138,7 +1138,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount, merchant) => `eliminó un gasto (${amount} para ${merchant})`, movedFromReport: (reportName) => `movió un gasto${reportName ? ` desde ${reportName}` : ''}`, movedTransactionTo: (reportUrl, reportName) => `movió este gasto${reportName ? ` a ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl, reportName) => + movedTransactionFrom: (reportUrl, reportName) => `movió este gasto${reportName ? ` desde ${reportName}` : ''}`, + movedTransactionFromPendingMatch: (reportUrl, reportName) => `movió este gasto${reportName ? ` desde ${reportName}` : ''} pendiente de coincidencia con una transacción de tarjeta de crédito`, unreportedTransaction: (reportUrl) => `movió este gasto a tu espacio personal`, movedAction: (shouldHideMovedReportUrl, movedReportUrl, newParentReportUrl, toPolicyName) => { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 2afc083f0807f..2a962750d469c 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1267,7 +1267,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `a supprimé une dépense (${amount} pour ${merchant})`, movedFromReport: (reportName: string) => `a déplacé une dépense${reportName ? `de ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `a déplacé cette dépense${reportName ? `vers ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => + movedTransactionFrom: (reportUrl: string, reportName?: string) => `a déplacé cette dépense${reportName ? `depuis ${reportName}` : ''}`, + movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => `a déplacé cette dépense${reportName ? `depuis ${reportName}` : ''} en attente de rapprochement avec une transaction par carte de crédit`, unreportedTransaction: (reportUrl: string) => `a déplacé cette dépense vers votre espace personnel`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { diff --git a/src/languages/it.ts b/src/languages/it.ts index f454ab3bf54b2..a56ca459c999f 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1261,7 +1261,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `ha eliminato una spesa (${amount} per ${merchant})`, movedFromReport: (reportName: string) => `ha spostato una spesa${reportName ? `da ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `ha spostato questa spesa${reportName ? `a ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => + movedTransactionFrom: (reportUrl: string, reportName?: string) => `ha spostato questa spesa${reportName ? `da ${reportName}` : ''}`, + movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => `ha spostato questa spesa${reportName ? `da ${reportName}` : ''} in attesa di abbinamento con una transazione della carta di credito`, unreportedTransaction: (reportUrl: string) => `ha spostato questa spesa nel tuo spazio personale`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index d213f8ed9a7fb..cec4e27f244aa 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1253,7 +1253,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `経費を削除しました(${merchant} に ${amount})`, movedFromReport: (reportName: string) => `経費${reportName ? `${reportName} から` : ''}を移動しました`, movedTransactionTo: (reportUrl: string, reportName?: string) => `この経費を移動しました${reportName ? `${reportName}へ` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => + movedTransactionFrom: (reportUrl: string, reportName?: string) => `この経費を移動しました${reportName ? `${reportName} から` : ''}`, + movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => `この経費を移動しました${reportName ? `${reportName} から` : ''}(クレジットカード取引との照合待ち)`, unreportedTransaction: (reportUrl: string) => `この経費をあなたの個人スペースに移動しました`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index e67847697522f..a8d1383b602d2 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1260,7 +1260,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `heeft een uitgave verwijderd (${amount} voor ${merchant})`, movedFromReport: (reportName: string) => `heeft een uitgave verplaatst${reportName ? `van ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `heeft deze uitgave verplaatst${reportName ? `naar ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => + movedTransactionFrom: (reportUrl: string, reportName?: string) => `heeft deze uitgave verplaatst${reportName ? `van ${reportName}` : ''}`, + movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => `heeft deze uitgave verplaatst${reportName ? `van ${reportName}` : ''} in afwachting van koppeling met een creditcardtransactie`, unreportedTransaction: (reportUrl: string) => `heeft deze uitgave verplaatst naar je persoonlijke ruimte`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index f0e54d59849eb..857bd858c2b87 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1260,7 +1260,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `usunął(-ę) wydatek (${amount} dla ${merchant})`, movedFromReport: (reportName: string) => `przeniesiono wydatek${reportName ? `z ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `przeniósł ten wydatek${reportName ? `do ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => + movedTransactionFrom: (reportUrl: string, reportName?: string) => `przeniósł(-ę) ten wydatek${reportName ? `z ${reportName}` : ''}`, + movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => `przeniósł(-ę) ten wydatek${reportName ? `z ${reportName}` : ''} w oczekiwaniu na dopasowanie z transakcją kartą kredytową`, unreportedTransaction: (reportUrl: string) => `przeniósł ten wydatek do Twojej przestrzeni osobistej`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 894dd550b08b1..fa200a9c80cef 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1259,7 +1259,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `excluiu uma despesa (${amount} em ${merchant})`, movedFromReport: (reportName: string) => `moveu uma despesa${reportName ? `de ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `moveu esta despesa${reportName ? `para ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => + movedTransactionFrom: (reportUrl: string, reportName?: string) => `moveu esta despesa${reportName ? `de ${reportName}` : ''}`, + movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => `moveu esta despesa${reportName ? `de ${reportName}` : ''} pendente de correspondência com uma transação de cartão de crédito`, unreportedTransaction: (reportUrl: string) => `moveu esta despesa para o seu espaço pessoal`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 5037a736474e4..d15f6d9f5c9dd 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1236,7 +1236,8 @@ const translations: TranslationDeepObject = { deletedTransaction: (amount: string, merchant: string) => `已删除一笔报销(${merchant} 的 ${amount})`, movedFromReport: (reportName: string) => `已移动一笔报销${reportName ? `来自${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `到 ${reportName}` : ''}`, - movedTransactionFrom: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `来自 ${reportName}` : ''},等待与信用卡交易匹配`, + movedTransactionFrom: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `来自 ${reportName}` : ''}`, + movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `来自 ${reportName}` : ''},等待与信用卡交易匹配`, unreportedTransaction: (reportUrl: string) => `已将此报销移动到你的个人空间`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 910024b7f1cdc..19a4ef3e3dee0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6915,7 +6915,7 @@ function getDeletedTransactionMessage(translate: LocalizedTranslate, action: Rep function getMovedTransactionMessage(translate: LocalizedTranslate, action: ReportAction) { const movedTransactionOriginalMessage = getOriginalMessage(action) ?? {}; - const {toReportID, fromReportID} = movedTransactionOriginalMessage as OriginalMessageMovedTransaction; + const {toReportID, fromReportID, reasoning} = movedTransactionOriginalMessage as OriginalMessageMovedTransaction; const toReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${toReportID}`]; const fromReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${fromReportID}`]; @@ -6929,6 +6929,9 @@ function getMovedTransactionMessage(translate: LocalizedTranslate, action: Repor if (typeof fromReportID === 'undefined') { return translate('iou.movedTransactionTo', reportUrl, reportName); } + if (reasoning === CONST.MOVED_TRANSACTION_REASONS.PENDING_CARD_MATCH) { + return translate('iou.movedTransactionFromPendingMatch', reportUrl, reportName); + } return translate('iou.movedTransactionFrom', reportUrl, reportName); } From e57101c1d4fc36a31e3dee96f5c7f3e507a6d5ab Mon Sep 17 00:00:00 2001 From: "huult (via MelvinBot)" Date: Fri, 13 Mar 2026 04:14:59 +0000 Subject: [PATCH 05/11] Fix: run prettier on zh-hans.ts to fix line length formatting Co-authored-by: huult --- src/languages/zh-hans.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index d15f6d9f5c9dd..cd0d69eea137c 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1237,7 +1237,8 @@ const translations: TranslationDeepObject = { movedFromReport: (reportName: string) => `已移动一笔报销${reportName ? `来自${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `到 ${reportName}` : ''}`, movedTransactionFrom: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `来自 ${reportName}` : ''}`, - movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `来自 ${reportName}` : ''},等待与信用卡交易匹配`, + movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => + `已移动此报销${reportName ? `来自 ${reportName}` : ''},等待与信用卡交易匹配`, unreportedTransaction: (reportUrl: string) => `已将此报销移动到你的个人空间`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { From a16f7baa05f0b6ed8981e1812097eaa555418f2b Mon Sep 17 00:00:00 2001 From: "huult (via MelvinBot)" Date: Fri, 13 Mar 2026 05:01:44 +0000 Subject: [PATCH 06/11] Address Codex review: deduplicate RTER confirmation and mark-as-cash logic - Extract markPendingRTERTransactionsAsCash into a shared action in Transaction.ts so both MoneyReportHeader and MoneyRequestReportPreviewContent call the same function - Add confirmPendingRTERAndProceed helper in both components to consolidate the repeated showConfirmModal pattern - Remove unused imports (getIOUActionForTransactionID, hasPendingRTERViolation, markAsCash) from components that no longer need them directly Co-authored-by: huult --- src/components/MoneyReportHeader.tsx | 73 ++++++--------- .../MoneyRequestReportPreviewContent.tsx | 92 ++++++++----------- src/libs/actions/Transaction.ts | 22 ++++- 3 files changed, 87 insertions(+), 100 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 61d340c238b90..b49ca9ebec57c 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -65,7 +65,6 @@ import {selectPaymentType} from '@libs/PaymentUtils'; import {getConnectedIntegration, getValidConnectedIntegration, hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; import { getIOUActionForReportID, - getIOUActionForTransactionID, getOriginalMessage, getReportAction, hasPendingDEWApprove, @@ -114,7 +113,6 @@ import { hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, hasCustomUnitOutOfPolicyViolation as hasCustomUnitOutOfPolicyViolationTransactionUtils, hasDuplicateTransactions, - hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, isDistanceRequest, isDuplicate, isExpensifyCardTransaction, @@ -144,7 +142,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'; @@ -788,18 +786,8 @@ function MoneyReportHeader({ markAsCashAction(iouTransactionID, reportID, transactionViolations); }, [iouTransactionID, requestParentReportAction, transactionThreadReport?.reportID, transactionViolations]); - const markPendingRTERTransactionsAsCash = useCallback(() => { - for (const t of transactions) { - const txViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t.transactionID}`]; - if (!hasPendingRTERViolationTransactionUtils(txViolations)) { - continue; - } - const action = getIOUActionForTransactionID(reportActions, t.transactionID); - const threadReportID = action?.childReportID; - if (threadReportID) { - markAsCashAction(t.transactionID, threadReportID, txViolations ?? []); - } - } + const handleMarkPendingRTERTransactionsAsCash = useCallback(() => { + markPendingRTERTransactionsAsCash(transactions, allTransactionViolations, reportActions); }, [transactions, allTransactionViolations, reportActions]); const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); @@ -1219,6 +1207,27 @@ function MoneyReportHeader({ startSubmittingAnimation, ]); + const confirmPendingRTERAndProceed = 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) { + handleMarkPendingRTERTransactionsAsCash(); + } + onProceed(); + }); + }, + [hasAnyPendingRTERViolation, showConfirmModal, translate, handleMarkPendingRTERTransactionsAsCash], + ); + const primaryActionsImplementation = { [CONST.REPORT.PRIMARY_ACTIONS.SUBMIT]: ( { - if (result.action === ModalActions.CONFIRM) { - markPendingRTERTransactionsAsCash(); - } - proceedWithSubmit(); - }); - return; - } - proceedWithSubmit(); + confirmPendingRTERAndProceed(proceedWithSubmit); }} isSubmittingAnimationRunning={isSubmittingAnimationRunning} onAnimationFinish={stopAnimation} @@ -1512,21 +1507,9 @@ function MoneyReportHeader({ showDWEModal(); return; } - if (hasAnyPendingRTERViolation) { - showConfirmModal({ - title: translate('iou.pendingMatchSubmitTitle'), - prompt: translate('iou.pendingMatchSubmitDescription'), - confirmText: translate('common.yes'), - cancelText: translate('common.no'), - }).then((result) => { - if (result.action === ModalActions.CONFIRM) { - markPendingRTERTransactionsAsCash(); - } - submitReport(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, userBillingGraceEndPeriods, amountOwed); - }); - return; - } - submitReport(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, userBillingGraceEndPeriods, amountOwed); + confirmPendingRTERAndProceed(() => { + submitReport(moneyRequestReport, policy, accountID, email ?? '', hasViolations, isASAPSubmitBetaEnabled, nextStep, userBillingGraceEndPeriods, amountOwed); + }); }, }, [CONST.REPORT.SECONDARY_ACTIONS.APPROVE]: { diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index a8cbd6cd0fa62..29ed9a9ee48ae 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -48,7 +48,7 @@ import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import {getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getConnectedIntegration, hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; -import {getIOUActionForTransactionID, hasPendingDEWSubmit} from '@libs/ReportActionsUtils'; +import {hasPendingDEWSubmit} from '@libs/ReportActionsUtils'; import {getInvoicePayerName} from '@libs/ReportNameUtils'; import getReportPreviewAction from '@libs/ReportPreviewActionUtils'; import { @@ -81,7 +81,6 @@ import {startSpan} from '@libs/telemetry/activeSpans'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import { hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, - hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, hasPendingUI, isManagedCardTransaction, isPending, @@ -90,7 +89,7 @@ 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 {markAsCash as markAsCashAction} from '@userActions/Transaction'; +import {markPendingRTERTransactionsAsCash} from '@userActions/Transaction'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -269,21 +268,31 @@ function MoneyRequestReportPreviewContent({ const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`); - const markPendingRTERTransactionsAsCash = useCallback(() => { - const reportActionsArray = Object.values(reportActions ?? {}); - for (const t of transactions) { - const txViolations = transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t.transactionID}`]; - if (!hasPendingRTERViolationTransactionUtils(txViolations)) { - continue; - } - const iouAction = getIOUActionForTransactionID(reportActionsArray, t.transactionID); - const threadReportID = iouAction?.childReportID; - if (threadReportID) { - markAsCashAction(t.transactionID, threadReportID, txViolations ?? []); - } - } + const handleMarkPendingRTERTransactionsAsCash = useCallback(() => { + markPendingRTERTransactionsAsCash(transactions, transactionViolations, Object.values(reportActions ?? {})); }, [transactions, transactionViolations, reportActions]); + const confirmPendingRTERAndProceed = 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) { + handleMarkPendingRTERTransactionsAsCash(); + } + onProceed(); + }); + }, + [hasAnyPendingRTERViolation, showConfirmModal, translate, 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( @@ -777,43 +786,20 @@ function MoneyRequestReportPreviewContent({ showDEWModal(); return; } - if (hasAnyPendingRTERViolation) { - showConfirmModal({ - title: translate('iou.pendingMatchSubmitTitle'), - prompt: translate('iou.pendingMatchSubmitDescription'), - confirmText: translate('common.yes'), - cancelText: translate('common.no'), - }).then((result) => { - if (result.action === ModalActions.CONFIRM) { - markPendingRTERTransactionsAsCash(); - } - startSubmittingAnimation(); - submitReport( - iouReport, - policy, - currentUserAccountID, - currentUserEmail, - hasViolations, - isASAPSubmitBetaEnabled, - iouReportNextStep, - userBillingGraceEndPeriods, - amountOwed, - ); - }); - return; - } - startSubmittingAnimation(); - submitReport( - iouReport, - policy, - currentUserAccountID, - currentUserEmail, - hasViolations, - isASAPSubmitBetaEnabled, - iouReportNextStep, - userBillingGraceEndPeriods, - amountOwed, - ); + confirmPendingRTERAndProceed(() => { + startSubmittingAnimation(); + submitReport( + iouReport, + policy, + currentUserAccountID, + currentUserEmail, + hasViolations, + isASAPSubmitBetaEnabled, + iouReportNextStep, + userBillingGraceEndPeriods, + amountOwed, + ); + }); }} isSubmittingAnimationRunning={isSubmittingAnimationRunning} onAnimationFinish={stopAnimation} diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index fb1ac93a07a78..de3cde62a87ca 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -19,7 +19,7 @@ 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 +34,7 @@ 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'; @@ -767,6 +767,23 @@ 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) { + 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: [ @@ -1625,6 +1642,7 @@ export { updateWaypoints, clearError, markAsCash, + markPendingRTERTransactionsAsCash, dismissDuplicateTransactionViolation, getDraftTransactions, generateTransactionID, From 62e59dc432a85590bd120ae5f89161ab7abe0993 Mon Sep 17 00:00:00 2001 From: "huult (via MelvinBot)" Date: Fri, 13 Mar 2026 05:03:49 +0000 Subject: [PATCH 07/11] Fix: run Prettier on files with formatting issues Co-authored-by: huult --- src/components/MoneyReportHeader.tsx | 9 +-------- .../MoneyRequestReportPreviewContent.tsx | 7 +------ src/libs/actions/Transaction.ts | 18 ++++++++++++++++-- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index b49ca9ebec57c..1bcb27bbfb152 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -63,14 +63,7 @@ import { import type {KYCFlowEvent, TriggerKYCFlow} from '@libs/PaymentUtils'; import {selectPaymentType} from '@libs/PaymentUtils'; import {getConnectedIntegration, getValidConnectedIntegration, hasDynamicExternalWorkflow} from '@libs/PolicyUtils'; -import { - getIOUActionForReportID, - getOriginalMessage, - getReportAction, - hasPendingDEWApprove, - hasPendingDEWSubmit, - isMoneyRequestAction, -} from '@libs/ReportActionsUtils'; +import {getIOUActionForReportID, getOriginalMessage, getReportAction, hasPendingDEWApprove, hasPendingDEWSubmit, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {getAllExpensesToHoldIfApplicable, getReportPrimaryAction, isMarkAsResolvedAction} from '@libs/ReportPrimaryActionUtils'; import {getSecondaryExportReportActions, getSecondaryReportActions} from '@libs/ReportSecondaryActionUtils'; import { diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index 29ed9a9ee48ae..0e428c4f4c511 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -79,12 +79,7 @@ import { import shouldAdjustScroll from '@libs/shouldAdjustScroll'; import {startSpan} from '@libs/telemetry/activeSpans'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; -import { - hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, - 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'; diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index de3cde62a87ca..1d9af741e9e2c 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, getIOUActionForTransactionID, 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 {hasPendingRTERViolation, 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'; From 29b45ecb39fe185b36b647bfbf3acd9c81ae1a31 Mon Sep 17 00:00:00 2001 From: "huult (via MelvinBot)" Date: Fri, 13 Mar 2026 05:07:33 +0000 Subject: [PATCH 08/11] Fix: guard against missing transactionID instead of defaulting to empty string Avoids violating the no-default-id-values ESLint rule by adding an early continue when transactionID is falsy. Co-authored-by: huult --- src/libs/actions/Transaction.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 1d9af741e9e2c..1a66eda85b358 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -786,14 +786,17 @@ function markAsCash(transactionID: string | undefined, transactionThreadReportID */ function markPendingRTERTransactionsAsCash(transactions: Array>, violationsCollection: OnyxCollection, reportActions: ReportAction[]) { for (const t of transactions) { - const txViolations = violationsCollection?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t?.transactionID}`]; + if (!t?.transactionID) { + continue; + } + const txViolations = violationsCollection?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t.transactionID}`]; if (!hasPendingRTERViolation(txViolations)) { continue; } - const action = getIOUActionForTransactionID(reportActions, t?.transactionID ?? ''); + const action = getIOUActionForTransactionID(reportActions, t.transactionID); const threadReportID = action?.childReportID; if (threadReportID) { - markAsCash(t?.transactionID, threadReportID, txViolations ?? []); + markAsCash(t.transactionID, threadReportID, txViolations ?? []); } } } From 5db8fc65a49f4204282d29f4e69690ebd403c11f Mon Sep 17 00:00:00 2001 From: "huult (via MelvinBot)" Date: Fri, 13 Mar 2026 14:04:26 +0000 Subject: [PATCH 09/11] Remove automatic case moved transaction message changes Remove the movedTransactionFromPendingMatch translation key from all locale files, the MOVED_TRANSACTION_REASONS constant, and the reasoning check in getMovedTransactionMessage() as this is no longer needed. Co-authored-by: huult --- src/CONST/index.ts | 3 --- src/languages/de.ts | 2 -- src/languages/en.ts | 2 -- src/languages/es.ts | 2 -- src/languages/fr.ts | 2 -- src/languages/it.ts | 2 -- src/languages/ja.ts | 2 -- src/languages/nl.ts | 2 -- src/languages/pl.ts | 2 -- src/languages/pt-BR.ts | 2 -- src/languages/zh-hans.ts | 2 -- src/libs/ReportUtils.ts | 5 +---- 12 files changed, 1 insertion(+), 27 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 5be26b02f6e3e..aa699de640f12 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6287,9 +6287,6 @@ const CONST = { BROKEN_CARD_CONNECTION_530: 'brokenCardConnection530', SEVEN_DAY_HOLD: 'sevenDayHold', }, - MOVED_TRANSACTION_REASONS: { - PENDING_CARD_MATCH: 'pendingCardMatch', - }, REVIEW_DUPLICATES_ORDER: ['merchant', 'category', 'tag', 'description', 'taxCode', 'billable', 'reimbursable'], REPORT_VIOLATIONS: { diff --git a/src/languages/de.ts b/src/languages/de.ts index 8c98c434c3b30..c05ee488669de 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1264,8 +1264,6 @@ const translations: TranslationDeepObject = { movedFromReport: (reportName: string) => `hat eine Ausgabe verschoben${reportName ? `von ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `hat diese Ausgabe verschoben${reportName ? `zu ${reportName}` : ''}`, movedTransactionFrom: (reportUrl: string, reportName?: string) => `hat diese Ausgabe verschoben${reportName ? `von ${reportName}` : ''}`, - movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => - `hat diese Ausgabe verschoben${reportName ? `von ${reportName}` : ''} ausstehende Zuordnung mit einer Kreditkartentransaktion`, unreportedTransaction: (reportUrl: string) => `hat diese Ausgabe in deinen persönlichen Bereich verschoben`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { diff --git a/src/languages/en.ts b/src/languages/en.ts index 304822e2a61e2..f52e1b0a32c5c 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1294,8 +1294,6 @@ const translations = { movedFromReport: (reportName: string) => `moved an expense${reportName ? ` from ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `moved this expense${reportName ? ` to ${reportName}` : ''}`, movedTransactionFrom: (reportUrl: string, reportName?: string) => `moved this expense${reportName ? ` from ${reportName}` : ''}`, - movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => - `moved this expense${reportName ? ` from ${reportName}` : ''} pending a match with a credit card transaction`, unreportedTransaction: (reportUrl: string) => `moved this expense to your personal space`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { diff --git a/src/languages/es.ts b/src/languages/es.ts index 1644206ac8b08..4f6542a99efda 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1139,8 +1139,6 @@ const translations: TranslationDeepObject = { movedFromReport: (reportName) => `movió un gasto${reportName ? ` desde ${reportName}` : ''}`, movedTransactionTo: (reportUrl, reportName) => `movió este gasto${reportName ? ` a ${reportName}` : ''}`, movedTransactionFrom: (reportUrl, reportName) => `movió este gasto${reportName ? ` desde ${reportName}` : ''}`, - movedTransactionFromPendingMatch: (reportUrl, reportName) => - `movió este gasto${reportName ? ` desde ${reportName}` : ''} pendiente de coincidencia con una transacción de tarjeta de crédito`, unreportedTransaction: (reportUrl) => `movió este gasto a tu espacio personal`, movedAction: (shouldHideMovedReportUrl, movedReportUrl, newParentReportUrl, toPolicyName) => { if (shouldHideMovedReportUrl) { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 2a962750d469c..cd218b5c22532 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1268,8 +1268,6 @@ const translations: TranslationDeepObject = { movedFromReport: (reportName: string) => `a déplacé une dépense${reportName ? `de ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `a déplacé cette dépense${reportName ? `vers ${reportName}` : ''}`, movedTransactionFrom: (reportUrl: string, reportName?: string) => `a déplacé cette dépense${reportName ? `depuis ${reportName}` : ''}`, - movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => - `a déplacé cette dépense${reportName ? `depuis ${reportName}` : ''} en attente de rapprochement avec une transaction par carte de crédit`, unreportedTransaction: (reportUrl: string) => `a déplacé cette dépense vers votre espace personnel`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { diff --git a/src/languages/it.ts b/src/languages/it.ts index a56ca459c999f..c63c9bd20661f 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1262,8 +1262,6 @@ const translations: TranslationDeepObject = { movedFromReport: (reportName: string) => `ha spostato una spesa${reportName ? `da ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `ha spostato questa spesa${reportName ? `a ${reportName}` : ''}`, movedTransactionFrom: (reportUrl: string, reportName?: string) => `ha spostato questa spesa${reportName ? `da ${reportName}` : ''}`, - movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => - `ha spostato questa spesa${reportName ? `da ${reportName}` : ''} in attesa di abbinamento con una transazione della carta di credito`, unreportedTransaction: (reportUrl: string) => `ha spostato questa spesa nel tuo spazio personale`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index cec4e27f244aa..a2760c587886e 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1254,8 +1254,6 @@ const translations: TranslationDeepObject = { movedFromReport: (reportName: string) => `経費${reportName ? `${reportName} から` : ''}を移動しました`, movedTransactionTo: (reportUrl: string, reportName?: string) => `この経費を移動しました${reportName ? `${reportName}へ` : ''}`, movedTransactionFrom: (reportUrl: string, reportName?: string) => `この経費を移動しました${reportName ? `${reportName} から` : ''}`, - movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => - `この経費を移動しました${reportName ? `${reportName} から` : ''}(クレジットカード取引との照合待ち)`, unreportedTransaction: (reportUrl: string) => `この経費をあなたの個人スペースに移動しました`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index a8d1383b602d2..7995b9c3909d1 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1261,8 +1261,6 @@ const translations: TranslationDeepObject = { movedFromReport: (reportName: string) => `heeft een uitgave verplaatst${reportName ? `van ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `heeft deze uitgave verplaatst${reportName ? `naar ${reportName}` : ''}`, movedTransactionFrom: (reportUrl: string, reportName?: string) => `heeft deze uitgave verplaatst${reportName ? `van ${reportName}` : ''}`, - movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => - `heeft deze uitgave verplaatst${reportName ? `van ${reportName}` : ''} in afwachting van koppeling met een creditcardtransactie`, unreportedTransaction: (reportUrl: string) => `heeft deze uitgave verplaatst naar je persoonlijke ruimte`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 857bd858c2b87..533c6d531779f 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1261,8 +1261,6 @@ const translations: TranslationDeepObject = { movedFromReport: (reportName: string) => `przeniesiono wydatek${reportName ? `z ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `przeniósł ten wydatek${reportName ? `do ${reportName}` : ''}`, movedTransactionFrom: (reportUrl: string, reportName?: string) => `przeniósł(-ę) ten wydatek${reportName ? `z ${reportName}` : ''}`, - movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => - `przeniósł(-ę) ten wydatek${reportName ? `z ${reportName}` : ''} w oczekiwaniu na dopasowanie z transakcją kartą kredytową`, unreportedTransaction: (reportUrl: string) => `przeniósł ten wydatek do Twojej przestrzeni osobistej`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index fa200a9c80cef..3fed5c69e4db8 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1260,8 +1260,6 @@ const translations: TranslationDeepObject = { movedFromReport: (reportName: string) => `moveu uma despesa${reportName ? `de ${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `moveu esta despesa${reportName ? `para ${reportName}` : ''}`, movedTransactionFrom: (reportUrl: string, reportName?: string) => `moveu esta despesa${reportName ? `de ${reportName}` : ''}`, - movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => - `moveu esta despesa${reportName ? `de ${reportName}` : ''} pendente de correspondência com uma transação de cartão de crédito`, unreportedTransaction: (reportUrl: string) => `moveu esta despesa para o seu espaço pessoal`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index cd0d69eea137c..43087942b86d3 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1237,8 +1237,6 @@ const translations: TranslationDeepObject = { movedFromReport: (reportName: string) => `已移动一笔报销${reportName ? `来自${reportName}` : ''}`, movedTransactionTo: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `到 ${reportName}` : ''}`, movedTransactionFrom: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `来自 ${reportName}` : ''}`, - movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => - `已移动此报销${reportName ? `来自 ${reportName}` : ''},等待与信用卡交易匹配`, unreportedTransaction: (reportUrl: string) => `已将此报销移动到你的个人空间`, movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => { if (shouldHideMovedReportUrl) { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 19a4ef3e3dee0..910024b7f1cdc 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6915,7 +6915,7 @@ function getDeletedTransactionMessage(translate: LocalizedTranslate, action: Rep function getMovedTransactionMessage(translate: LocalizedTranslate, action: ReportAction) { const movedTransactionOriginalMessage = getOriginalMessage(action) ?? {}; - const {toReportID, fromReportID, reasoning} = movedTransactionOriginalMessage as OriginalMessageMovedTransaction; + const {toReportID, fromReportID} = movedTransactionOriginalMessage as OriginalMessageMovedTransaction; const toReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${toReportID}`]; const fromReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${fromReportID}`]; @@ -6929,9 +6929,6 @@ function getMovedTransactionMessage(translate: LocalizedTranslate, action: Repor if (typeof fromReportID === 'undefined') { return translate('iou.movedTransactionTo', reportUrl, reportName); } - if (reasoning === CONST.MOVED_TRANSACTION_REASONS.PENDING_CARD_MATCH) { - return translate('iou.movedTransactionFromPendingMatch', reportUrl, reportName); - } return translate('iou.movedTransactionFrom', reportUrl, reportName); } From 651b282eeb49ba3ac87399bd98f852dbfa599d78 Mon Sep 17 00:00:00 2001 From: "huult (via MelvinBot)" Date: Fri, 13 Mar 2026 14:49:30 +0000 Subject: [PATCH 10/11] Fix: make hasAnyPendingRTERViolation mark-as-cash aware Use getTransactionViolations (which filters out dismissed violations) instead of reading raw violations from Onyx. This ensures that if a transaction was already marked as cash, the submit confirmation modal is not shown again for that transaction. Co-authored-by: huult --- src/components/MoneyReportHeader.tsx | 5 ++++- .../MoneyRequestReportPreviewContent.tsx | 5 ++++- src/libs/TransactionUtils/index.ts | 15 +++++++++++---- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 1bcb27bbfb152..a77d571721e9c 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -447,7 +447,10 @@ function MoneyReportHeader({ [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), [transactions, allTransactionViolations]); + 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); diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index 0e428c4f4c511..465cb419eb83f 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -204,7 +204,10 @@ 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), [transactions, transactionViolations]); + 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}); diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 03397eb8cdcbd..b3d891b1e1e23 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1542,12 +1542,19 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | } /** - * Check if any of the given transactions have a pending RTER violation. + * 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): boolean { +function hasAnyPendingRTERViolation( + transactions: Array>, + allTransactionViolations: OnyxCollection, + currentUserEmail: string, + currentUserAccountID: number, + report: OnyxEntry, + policy: OnyxEntry, +): boolean { return transactions.some((t) => { - const txViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t?.transactionID}`]; - return hasPendingRTERViolation(txViolations); + const filteredViolations = getTransactionViolations(t, allTransactionViolations, currentUserEmail, currentUserAccountID, report, policy); + return hasPendingRTERViolation(filteredViolations); }); } From 14d30ab388b79a95dce91bc20daa79d4aab3caa0 Mon Sep 17 00:00:00 2001 From: "huult (via MelvinBot)" Date: Thu, 19 Mar 2026 00:59:43 +0000 Subject: [PATCH 11/11] Refactor: extract useConfirmPendingRTERAndProceed hook to deduplicate confirm modal logic Both MoneyReportHeader and MoneyRequestReportPreviewContent had identical confirmPendingRTERAndProceed callbacks. Extract this shared pattern into a reusable hook. Co-authored-by: huult --- src/components/MoneyReportHeader.tsx | 22 ++---------- .../MoneyRequestReportPreviewContent.tsx | 22 ++---------- src/hooks/useConfirmPendingRTERAndProceed.ts | 36 +++++++++++++++++++ 3 files changed, 40 insertions(+), 40 deletions(-) create mode 100644 src/hooks/useConfirmPendingRTERAndProceed.ts diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index c0123c76fee02..f442e5e4c8b74 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -12,6 +12,7 @@ import type {OnyxEntry} from 'react-native-onyx'; 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'; @@ -875,26 +876,7 @@ function MoneyReportHeader({ markPendingRTERTransactionsAsCash(transactions, allTransactionViolations, reportActions); }, [transactions, allTransactionViolations, reportActions]); - const confirmPendingRTERAndProceed = 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) { - handleMarkPendingRTERTransactionsAsCash(); - } - onProceed(); - }); - }, - [hasAnyPendingRTERViolation, showConfirmModal, translate, handleMarkPendingRTERTransactionsAsCash], - ); + const confirmPendingRTERAndProceed = useConfirmPendingRTERAndProceed(hasAnyPendingRTERViolation, handleMarkPendingRTERTransactionsAsCash); const handleSubmitReport = useCallback( (skipAnimation = false) => { diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index f93b09fa65620..7f5d9c9276825 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'; @@ -270,26 +271,7 @@ function MoneyRequestReportPreviewContent({ markPendingRTERTransactionsAsCash(transactions, transactionViolations, Object.values(reportActions ?? {})); }, [transactions, transactionViolations, reportActions]); - const confirmPendingRTERAndProceed = 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) { - handleMarkPendingRTERTransactionsAsCash(); - } - onProceed(); - }); - }, - [hasAnyPendingRTERViolation, showConfirmModal, translate, handleMarkPendingRTERTransactionsAsCash], - ); + 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 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;