Skip to content

Improve RTER messaging and add submit confirmation for pending card matches#85167

Merged
stitesExpensify merged 16 commits intomainfrom
claude-improveRterMovedTransactionMessage
Mar 24, 2026
Merged

Improve RTER messaging and add submit confirmation for pending card matches#85167
stitesExpensify merged 16 commits intomainfrom
claude-improveRterMovedTransactionMessage

Conversation

@MelvinBot
Copy link
Copy Markdown
Contributor

@MelvinBot MelvinBot commented Mar 13, 2026

Explanation of Change

This PR adds a confirmation modal before submitting a report that contains expenses with pending RTER (Receipt-To-Expense-Receipt) violations. When the user clicks Submit (in both MoneyReportHeader and MoneyRequestReportPreviewContent), if any expenses are awaiting a credit card match:

  • A modal appears: "Some expenses are awaiting a match with a credit card transaction. Do you want to mark them as cash?"
  • Yes: Marks each pending-RTER expense as cash, then submits the report
  • No: Submits the report without marking expenses as cash

Both components (MoneyReportHeader.tsx primary + secondary submit, MoneyRequestReportPreviewContent.tsx submit) have consistent behavior.

Fixed Issues

$ #83108

PROPOSAL: #83108 (comment)

Tests

  1. Open a workspace with a company card assigned
  2. Create a manual/cash expense (don't click "mark as cash") so it has a pending RTER violation
  3. Go to the report containing the expense
  4. Click Submit
  5. Verify a confirmation modal appears with title "Submit report" and body "Some expenses are awaiting a match with a credit card transaction. Do you want to mark them as cash?"
  6. Click Yes → Verify the RTER-pending expenses are marked as cash and the report is submitted
  7. Repeat steps 3-5, but click No → Verify the report is submitted without marking expenses as cash
  8. Verify the same behavior when submitting from the report preview (LHN)
  9. Verify the same behavior when using the secondary (dropdown) Submit action
  • Verify that no errors appear in the JS console

Offline tests

  1. Go offline
  2. Attempt to submit a report with RTER-pending expenses
  3. Verify the confirmation modal still appears
  4. Click Yes or No → Verify the actions are queued for when the device comes back online

QA Steps

  1. Assign a company card to a cardholder on a workspace
  2. As the cardholder, create a cash expense (don't click "mark as cash")
  3. Go to the report and click Submit
  4. Verify a confirmation modal appears asking about marking expenses as cash
  5. Test both Yes and No paths
  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

…atches

- 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 <huult@users.noreply.github.com>
@OSBotify

This comment has been minimized.

@MelvinBot
Copy link
Copy Markdown
Contributor Author

The failing checks are unrelated to this PR's changes.

PR changes: MoneyReportHeader.tsx, MoneyRequestReportPreviewContent.tsx, and language files (src/languages/*.ts)

Failing tests:

  1. tests/ui/NewChatPageTest.tsx (job 6) — Unable to find an element with accessibility label: qa@expensify.com. This is a flaky UI test with a timing/race condition unrelated to the RTER messaging changes in this PR.

  2. tests/ui/components/IOURequestStepConfirmationPageTest.tsx (job 7) — TypeError: Cannot read properties of undefined (reading 'GRANTED') at __mocks__/react-native-permissions.ts:11. This is a mock setup issue with react-native-permissions unrelated to this PR.

  3. checklist — PR Author/Reviewer checklist, not a code issue.

Evidence: All three recent test runs on main pass successfully. Neither failing test file is touched by this PR, and neither test exercises code paths modified by this PR.

These failures should resolve on a re-run, or can be investigated separately as flaky test issues.

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(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    const hasAnyPendingRTERViolation = useMemo(
        () =>
            transactions.some((t) => {
                const txViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${t.transactionID}`];
                return hasPendingRTERViolationTransactionUtils(txViolations);
            }),
        [transactions, allTransactionViolations],
    );

@MelvinBot It seems duplicated. If possible, please create a utility function so it can be reusable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted the duplicated logic into a reusable hasAnyPendingRTERViolation(transactions, allTransactionViolations) utility function in TransactionUtils. Both MoneyReportHeader and MoneyRequestReportPreviewContent now call this shared utility instead of inlining the same logic.

@MelvinBot
Copy link
Copy Markdown
Contributor Author

The failing checks (excluding checklist which is the PR Author/Reviewer Checklist) are unrelated to this PR's changes.

Failing tests:

  1. test (job 6)tests/ui/NewChatPageTest.tsx: Unable to find an element with accessibility label: qa@expensify.com

    • This test exercises the NewChatPage user search flow, which has no connection to the MoneyReportHeader, MoneyRequestReportPreviewContent, or language string changes in this PR.
  2. test (job 7)tests/ui/components/IOURequestStepConfirmationPageTest.tsx: TypeError: Cannot read properties of undefined (reading 'GRANTED') in __mocks__/react-native-permissions.ts:11

    • This is a mock infrastructure issue with react-native-permissions. The PR does not modify any permission-related code.

Evidence:

  • The PR only modifies MoneyReportHeader.tsx, MoneyRequestReportPreviewContent.tsx, and 10 language files — none of these are referenced by the failing tests.
  • The 3 most recent Jest test runs on main all pass successfully.
  • These appear to be flaky tests. Re-running the failed jobs should resolve them.

Move the duplicated hasAnyPendingRTERViolation logic from both
MoneyReportHeader and MoneyRequestReportPreviewContent into a
reusable utility function in TransactionUtils.

Co-authored-by: huult <huult@users.noreply.github.com>
@huult
Copy link
Copy Markdown
Contributor

huult commented Mar 13, 2026

@MelvinBot Ready for review

Use Array<OnyxEntry<Transaction>> instead of OnyxEntry<Transaction>[]
to satisfy @typescript-eslint/array-type rule, and fix Prettier
formatting in MoneyReportHeader and MoneyRequestReportPreviewContent.

Co-authored-by: huult <huult@users.noreply.github.com>
@MelvinBot
Copy link
Copy Markdown
Contributor Author

Fixed the failing checks:

  • ESLint (@typescript-eslint/array-type): Changed OnyxEntry<Transaction>[] to Array<OnyxEntry<Transaction>> in src/libs/TransactionUtils/index.ts:1547
  • Prettier: Fixed import formatting in MoneyReportHeader.tsx (alphabetical sort) and MoneyRequestReportPreviewContent.tsx (multi-line import)

@huult
Copy link
Copy Markdown
Contributor

huult commented Mar 13, 2026

@MelvinBot Please update Link to Slack message: in the PR checklist with this link: https://expensify.slack.com/archives/C01GTK53T8Q/p1773373798739869

@MelvinBot
Copy link
Copy Markdown
Contributor Author

Updated the PR checklist with the Slack message link: https://expensify.slack.com/archives/C01GTK53T8Q/p1773373798739869

@huult
Copy link
Copy Markdown
Contributor

huult commented Mar 13, 2026

Automatic case — improve moved transaction message:

Extend getMovedTransactionMessage() in ReportUtils.ts:6884-6901 to check for reasoning in the action's originalMessage. When reasoning indicates a pending card match (e.g., reasoning contains a specific identifier like 'pendingCardMatch'), use a new translation string such as: "moved this expense from %reportName% pending a match with a credit card transaction."

Add new translation strings for the RTER-specific moved message in en.ts and other locale files.

Backend (Auth repo): The reasoning field on OriginalMessageMovedTransaction must be populated by the server when harvesting moves an expense due to RTER hold. This is a server-side change in the Auth repo's harvesting logic.

@MelvinBot Please update this part in the PR. I think it's correct.

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 <huult@users.noreply.github.com>
@MelvinBot
Copy link
Copy Markdown
Contributor Author

Updated the moved transaction message logic as described:

  1. Reverted movedTransactionFrom — restored the original generic translation in all 10 locale files (it was previously modified to unconditionally include the pending match text).

  2. Added new movedTransactionFromPendingMatch translation key — in all locale files, containing the RTER-specific message (e.g., "moved this expense from <report> pending a match with a credit card transaction").

  3. Updated getMovedTransactionMessage() in ReportUtils.ts — now extracts the reasoning field from OriginalMessageMovedTransaction and conditionally uses the new movedTransactionFromPendingMatch translation when reasoning === 'pendingCardMatch'.

  4. Added CONST.MOVED_TRANSACTION_REASONS.PENDING_CARD_MATCH — a constant for the reasoning value that the backend (Auth repo) will populate when harvesting moves an expense due to RTER hold.

Note: The backend (Auth repo) still needs to populate the reasoning field with 'pendingCardMatch' on MOVED_TRANSACTION actions created during RTER-related harvesting moves.

@OSBotify
Copy link
Copy Markdown
Contributor

🦜 Polyglot Parrot! 🦜

Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues:

View the translation diff
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 8c98c434..61cad924 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -1265,7 +1265,7 @@ const translations: TranslationDeepObject<typeof en> = {
         movedTransactionTo: (reportUrl: string, reportName?: string) => `hat diese Ausgabe verschoben${reportName ? `zu <a href="${reportUrl}">${reportName}</a>` : ''}`,
         movedTransactionFrom: (reportUrl: string, reportName?: string) => `hat diese Ausgabe verschoben${reportName ? `von <a href="${reportUrl}">${reportName}</a>` : ''}`,
         movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) =>
-            `hat diese Ausgabe verschoben${reportName ? `von <a href="${reportUrl}">${reportName}</a>` : ''} ausstehende Zuordnung mit einer Kreditkartentransaktion`,
+            `hat diese Ausgabe${reportName ? `von <a href="${reportUrl}">${reportName}</a>` : ''} verschoben, bis sie mit einer Kreditkartentransaktion abgeglichen ist`,
         unreportedTransaction: (reportUrl: string) => `hat diese Ausgabe in deinen <a href="${reportUrl}">persönlichen Bereich</a> verschoben`,
         movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => {
             if (shouldHideMovedReportUrl) {
@@ -1277,8 +1277,8 @@ const translations: TranslationDeepObject<typeof en> = {
         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?',
+        pendingMatchSubmitTitle: 'Abrechnung einreichen',
+        pendingMatchSubmitDescription: 'Einige Ausgaben warten noch auf eine Zuordnung zu einer Kreditkartentransaktion. Möchten Sie sie als Barzahlung 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/fr.ts b/src/languages/fr.ts
index 2a962750..74236dd2 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -1269,7 +1269,7 @@ const translations: TranslationDeepObject<typeof en> = {
         movedTransactionTo: (reportUrl: string, reportName?: string) => `a déplacé cette dépense${reportName ? `vers <a href="${reportUrl}">${reportName}</a>` : ''}`,
         movedTransactionFrom: (reportUrl: string, reportName?: string) => `a déplacé cette dépense${reportName ? `depuis <a href="${reportUrl}">${reportName}</a>` : ''}`,
         movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) =>
-            `a déplacé cette dépense${reportName ? `depuis <a href="${reportUrl}">${reportName}</a>` : ''} en attente de rapprochement avec une transaction par carte de crédit`,
+            `a déplacé cette dépense${reportName ? `depuis <a href="${reportUrl}">${reportName}</a>` : ''} en attente d’une correspondance avec une transaction de carte de crédit`,
         unreportedTransaction: (reportUrl: string) => `a déplacé cette dépense vers votre <a href="${reportUrl}">espace personnel</a>`,
         movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => {
             if (shouldHideMovedReportUrl) {
@@ -1281,8 +1281,8 @@ const translations: TranslationDeepObject<typeof en> = {
         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 ?',
+        pendingMatchSubmitTitle: 'Soumettre la note de frais',
+        pendingMatchSubmitDescription: 'Certaines dépenses sont en attente de rapprochement avec une transaction de carte de crédit. Voulez-vous les marquer comme réglées en 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 a56ca459..0e863ba4 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -1263,7 +1263,7 @@ const translations: TranslationDeepObject<typeof en> = {
         movedTransactionTo: (reportUrl: string, reportName?: string) => `ha spostato questa spesa${reportName ? `a <a href="${reportUrl}">${reportName}</a>` : ''}`,
         movedTransactionFrom: (reportUrl: string, reportName?: string) => `ha spostato questa spesa${reportName ? `da <a href="${reportUrl}">${reportName}</a>` : ''}`,
         movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) =>
-            `ha spostato questa spesa${reportName ? `da <a href="${reportUrl}">${reportName}</a>` : ''} in attesa di abbinamento con una transazione della carta di credito`,
+            `ha spostato questa spesa${reportName ? `da <a href="${reportUrl}">${reportName}</a>` : ''} in attesa di un abbinamento con una transazione della carta di credito`,
         unreportedTransaction: (reportUrl: string) => `ha spostato questa spesa nel tuo <a href="${reportUrl}">spazio personale</a>`,
         movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => {
             if (shouldHideMovedReportUrl) {
@@ -1275,8 +1275,8 @@ const translations: TranslationDeepObject<typeof en> = {
         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?',
+        pendingMatchSubmitTitle: 'Invia rapporto',
+        pendingMatchSubmitDescription: 'Alcune spese sono in attesa di essere abbinate a una transazione con carta di credito. Vuoi contrassegnarle come pagate in contanti?',
         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 cec4e27f..2854f277 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -1255,7 +1255,7 @@ const translations: TranslationDeepObject<typeof en> = {
         movedTransactionTo: (reportUrl: string, reportName?: string) => `この経費を移動しました${reportName ? `<a href="${reportUrl}">${reportName}</a>へ` : ''}`,
         movedTransactionFrom: (reportUrl: string, reportName?: string) => `この経費を移動しました${reportName ? `<a href="${reportUrl}">${reportName}</a> から` : ''}`,
         movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) =>
-            `この経費を移動しました${reportName ? `<a href="${reportUrl}">${reportName}</a> から` : ''}(クレジットカード取引との照合待ち)`,
+            `この経費${reportName ? `from <a href="${reportUrl}">${reportName}</a>` : ''}を、クレジットカード取引との照合待ちに移動しました`,
         unreportedTransaction: (reportUrl: string) => `この経費をあなたの<a href="${reportUrl}">個人スペース</a>に移動しました`,
         movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => {
             if (shouldHideMovedReportUrl) {
@@ -1267,8 +1267,8 @@ const translations: TranslationDeepObject<typeof en> = {
         pendingMatch: '保留中の照合',
         pendingMatchWithCreditCardDescription: 'レシートはカード取引との照合待ちです。現金としてマークしてキャンセルします。',
         markAsCash: '現金としてマーク',
-        pendingMatchSubmitTitle: 'レポートを提出',
-        pendingMatchSubmitDescription: '一部の経費がクレジットカード取引との照合待ちです。現金としてマークしますか?',
+        pendingMatchSubmitTitle: 'レポートを送信',
+        pendingMatchSubmitDescription: '一部の経費がクレジットカード明細との照合待ちです。現金としてマークしますか?',
         routePending: 'ルート保留中…',
         automaticallyEnterExpenseDetails: 'コンシェルジュが自動的に経費の詳細を入力するか、手動で追加することができます。',
         receiptScanning: () => ({
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index a8d1383b..6384eaf5 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -1262,7 +1262,7 @@ const translations: TranslationDeepObject<typeof en> = {
         movedTransactionTo: (reportUrl: string, reportName?: string) => `heeft deze uitgave verplaatst${reportName ? `naar <a href="${reportUrl}">${reportName}</a>` : ''}`,
         movedTransactionFrom: (reportUrl: string, reportName?: string) => `heeft deze uitgave verplaatst${reportName ? `van <a href="${reportUrl}">${reportName}</a>` : ''}`,
         movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) =>
-            `heeft deze uitgave verplaatst${reportName ? `van <a href="${reportUrl}">${reportName}</a>` : ''} in afwachting van koppeling met een creditcardtransactie`,
+            `heeft deze uitgave${reportName ? `van <a href="${reportUrl}">${reportName}</a>` : ''} verplaatst in afwachting van een overeenkomst met een creditcardtransactie`,
         unreportedTransaction: (reportUrl: string) => `heeft deze uitgave verplaatst naar je <a href="${reportUrl}">persoonlijke ruimte</a>`,
         movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => {
             if (shouldHideMovedReportUrl) {
@@ -1275,7 +1275,7 @@ const translations: TranslationDeepObject<typeof en> = {
         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?',
+        pendingMatchSubmitDescription: 'Sommige uitgaven wachten op een match met een creditcardtransactie. Wil je 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 857bd858..dd257c71 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -1262,7 +1262,7 @@ const translations: TranslationDeepObject<typeof en> = {
         movedTransactionTo: (reportUrl: string, reportName?: string) => `przeniósł ten wydatek${reportName ? `do <a href="${reportUrl}">${reportName}</a>` : ''}`,
         movedTransactionFrom: (reportUrl: string, reportName?: string) => `przeniósł(-ę) ten wydatek${reportName ? `z <a href="${reportUrl}">${reportName}</a>` : ''}`,
         movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) =>
-            `przeniósł(-ę) ten wydatek${reportName ? `z <a href="${reportUrl}">${reportName}</a>` : ''} w oczekiwaniu na dopasowanie z transakcją kartą kredytową`,
+            `przeniósł/przeniosła ten wydatek${reportName ? `z <a href="${reportUrl}">${reportName}</a>` : ''}, oczekując na dopasowanie do transakcji z karty kredytowej`,
         unreportedTransaction: (reportUrl: string) => `przeniósł ten wydatek do Twojej <a href="${reportUrl}">przestrzeni osobistej</a>`,
         movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => {
             if (shouldHideMovedReportUrl) {
@@ -1275,7 +1275,7 @@ const translations: TranslationDeepObject<typeof en> = {
         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ę?',
+        pendingMatchSubmitDescription: 'Niektóre wydatki czekają na dopasowanie do transakcji z karty kredytowej. Czy chcesz oznaczyć je jako gotówkowe?',
         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 fa200a9c..8f434ab1 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -1261,7 +1261,7 @@ const translations: TranslationDeepObject<typeof en> = {
         movedTransactionTo: (reportUrl: string, reportName?: string) => `moveu esta despesa${reportName ? `para <a href="${reportUrl}">${reportName}</a>` : ''}`,
         movedTransactionFrom: (reportUrl: string, reportName?: string) => `moveu esta despesa${reportName ? `de <a href="${reportUrl}">${reportName}</a>` : ''}`,
         movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) =>
-            `moveu esta despesa${reportName ? `de <a href="${reportUrl}">${reportName}</a>` : ''} pendente de correspondência com uma transação de cartão de crédito`,
+            `moveu esta despesa${reportName ? `de <a href="${reportUrl}">${reportName}</a>` : ''} para pendente, aguardando correspondência com uma transação de cartão de crédito`,
         unreportedTransaction: (reportUrl: string) => `moveu esta despesa para o seu <a href="${reportUrl}">espaço pessoal</a>`,
         movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => {
             if (shouldHideMovedReportUrl) {
@@ -1274,7 +1274,7 @@ const translations: TranslationDeepObject<typeof en> = {
         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?',
+        pendingMatchSubmitDescription: 'Algumas despesas estão aguardando correspondência com uma transação de cartão de crédito. Você quer 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 d15f6d9f..dd79d169 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -1237,7 +1237,8 @@ const translations: TranslationDeepObject<typeof en> = {
         movedFromReport: (reportName: string) => `已移动一笔报销${reportName ? `来自${reportName}` : ''}`,
         movedTransactionTo: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `到 <a href="${reportUrl}">${reportName}</a>` : ''}`,
         movedTransactionFrom: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `来自 <a href="${reportUrl}">${reportName}</a>` : ''}`,
-        movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) => `已移动此报销${reportName ? `来自 <a href="${reportUrl}">${reportName}</a>` : ''},等待与信用卡交易匹配`,
+        movedTransactionFromPendingMatch: (reportUrl: string, reportName?: string) =>
+            `已将此报销单${reportName ? `来自 <a href="${reportUrl}">${reportName}</a>` : ''}移至“等待与信用卡交易匹配”状态`,
         unreportedTransaction: (reportUrl: string) => `已将此报销移动到你的<a href="${reportUrl}">个人空间</a>`,
         movedAction: (shouldHideMovedReportUrl: boolean, movedReportUrl: string, newParentReportUrl: string, toPolicyName: string) => {
             if (shouldHideMovedReportUrl) {
@@ -1249,8 +1250,8 @@ const translations: TranslationDeepObject<typeof en> = {
         pendingMatch: '待匹配',
         pendingMatchWithCreditCardDescription: '收据正在等待与卡片交易匹配。将其标记为现金以取消。',
         markAsCash: '标记为现金',
-        pendingMatchSubmitTitle: '提交报告',
-        pendingMatchSubmitDescription: '部分费用正在等待与信用卡交易匹配。您要将它们标记为现金吗?',
+        pendingMatchSubmitTitle: '提交报表',
+        pendingMatchSubmitDescription: '部分报销正在等待与信用卡交易匹配。你要将它们标记为现金支出吗?',
         routePending: '路由处理中…',
         automaticallyEnterExpenseDetails: 'Concierge 将自动为您输入费用详情,或者您可以手动添加。',
         receiptScanning: () => ({

Note

You can apply these changes to your branch by copying the patch to your clipboard, then running pbpaste | git apply 😉

View workflow run

Co-authored-by: huult <huult@users.noreply.github.com>
@MelvinBot
Copy link
Copy Markdown
Contributor Author

Fixed the failing Prettier check: the movedTransactionFromPendingMatch line in src/languages/zh-hans.ts exceeded the max line length and needed to be wrapped.

@huult
Copy link
Copy Markdown
Contributor

huult commented Mar 13, 2026

@MelvinBot Please mark this PR as ready for review

@MelvinBot MelvinBot marked this pull request as ready for review March 13, 2026 04:29
@MelvinBot MelvinBot requested a review from a team as a code owner March 13, 2026 04:29
@melvin-bot melvin-bot bot requested a review from huult March 13, 2026 04:29
@melvin-bot
Copy link
Copy Markdown

melvin-bot bot commented Mar 13, 2026

@huult Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot melvin-bot bot requested review from trjExpensify and removed request for a team March 13, 2026 04:29
@MelvinBot
Copy link
Copy Markdown
Contributor Author

Done — this PR has been marked as ready for review. 🤖

@MelvinBot
Copy link
Copy Markdown
Contributor Author

Reviewed and addressed the feedback:

Code change (pushed):

  • Extracted the duplicated confirmPendingRTERAndProceed callback into a shared useConfirmPendingRTERAndProceed hook — addresses stitesExpensify's DRY concern

Codex review responses (replied to each thread):

  • P1 (paginated reportActions): Not an issue — MoneyRequestReportPreviewContent reads the full Onyx collection, and MoneyReportHeader receives the full set via props. The paginated variant is only used for rendering the action list UI.
  • P2 (shouldShowViolation): hasAnyPendingRTERViolation already filters dismissed violations via getTransactionViolations(). The shouldShowViolation() gate is about UI display of warning badges, not about the actual transaction state that the submit confirmation cares about.
  • P2 (dismissed RTERs in markAsCash): The modal only triggers when filtered violations show a pending RTER. The mark-as-cash action correctly operates on the real transaction state. If a transaction was already marked as cash, the violation is removed entirely and gets skipped.

@trjExpensify
Copy link
Copy Markdown
Contributor

@huult what's the deal?

@huult
Copy link
Copy Markdown
Contributor

huult commented Mar 20, 2026

@MelvinBot please resolve the conflict

… conflict

Co-authored-by: huult <huult@users.noreply.github.com>
@MelvinBot
Copy link
Copy Markdown
Contributor Author

Resolved the merge conflict in MoneyRequestReportPreviewContent.tsx.

The conflict was between the PR's object-based action pattern and main's individual variable + switch pattern. I kept main's structure (individual variables with switch statement) while preserving the PR's key change: wrapping submitReport with confirmPendingRTERAndProceed in the submit action.

@MelvinBot
Copy link
Copy Markdown
Contributor Author

The failing ESLint check is unrelated to this PR's changes.

Analysis: The full-repo ESLint check is failing with 316 warnings (0 errors) in files not modified by this PR:

  • WorkspaceOwnerChangeErrorPage.tsxreact-hooks/refs (ref access during render)
  • WorkspaceOwnerChangeSuccessPage.tsxreact-hooks/refs (ref access during render)
  • WorkspacePerDiemPage.tsxreact-hooks/set-state-in-effect (setState in effect)
  • ChangeReceiptBillingAccountPage.tsxreact-hooks/set-state-in-effect (setState in effect)

Evidence: This PR only modifies MoneyReportHeader.tsx, MoneyRequestReportPreviewContent.tsx, useConfirmPendingRTERAndProceed.ts, language files, and transaction libs. The Changed files ESLint check (which only lints files in the PR diff) passed successfully. These warnings are pre-existing on main.

@MelvinBot
Copy link
Copy Markdown
Contributor Author

Both failing checks are unrelated to this PR's changes.

1. ESLint check — The 316 warnings are all in files NOT modified by this PR (e.g., WorkspaceOwnerChangeErrorPage.tsx, WorkspaceOwnerChangeSuccessPage.tsx, WorkspacePerDiemPage.tsx, ChangeReceiptBillingAccountPage.tsx). The same ESLint failure is occurring across multiple other PRs right now, confirming it's a repo-wide issue — not caused by this PR. Note that the Changed files ESLint check (which only lints files in the PR diff) passed.

2. build (New Expensify Dev, DebugDevelopment, false) — The iOS build failed in the "Rock Remote Build" step with a remote cache error (NoSuchKey: The specified key does not exist). This is an infrastructure/CI issue, not a code problem. The PR only modifies TypeScript files and language translations, which cannot cause an iOS native build failure.

No code fix is needed from this PR. These failures should resolve when the underlying repo-wide ESLint warnings are addressed and the iOS build cache is refreshed.

@huult
Copy link
Copy Markdown
Contributor

huult commented Mar 23, 2026

@huult what's the deal?

@trjExpensify Melvin bot has updated the feedback, and I re-tested to make sure it works.

@huult
Copy link
Copy Markdown
Contributor

huult commented Mar 23, 2026

@huult what's the deal?

@trjExpensify Melvin bot has updated the feedback, and I re-tested to make sure it works.

It works. @stitesExpensify, it's all yours. Could you review it again?

@huult
Copy link
Copy Markdown
Contributor

huult commented Mar 23, 2026

@MelvinBot Please sync with main

…edTransactionMessage

Co-authored-by: huult <huult@users.noreply.github.com>
@MelvinBot
Copy link
Copy Markdown
Contributor Author

Synced with main — merged the latest main branch into this PR branch. The merge was clean (no conflicts).

@MelvinBot
Copy link
Copy Markdown
Contributor Author

The failing ESLint check is unrelated to this PR's changes.

Analysis: The failures are in files not modified by this PR:

  • WorkspaceOwnerChangeErrorPage.tsx:51react-hooks/refs (ref access during render)
  • WorkspaceOwnerChangeSuccessPage.tsx:45react-hooks/refs (ref access during render)
  • WorkspacePerDiemPage.tsx:386react-hooks/set-state-in-effect (setState in effect)
  • ChangeReceiptBillingAccountPage.tsx:95react-hooks/set-state-in-effect (setState in effect)

Evidence: This PR only modifies files in src/components/, src/hooks/, src/languages/, and src/libs/. None of the failing files are in the PR diff. These are pre-existing warnings (316 total) being treated as errors by the full repo ESLint CI check.

@MelvinBot
Copy link
Copy Markdown
Contributor Author

The failing check (ESLint check) is unrelated to this PR.

Analysis: The full-repo ESLint check is failing due to 316 pre-existing warnings (0 errors) in files not changed by this PR:

  • WorkspaceOwnerChangeErrorPage.tsxreact-hooks/refs (ref access during render)
  • WorkspaceOwnerChangeSuccessPage.tsxreact-hooks/refs (ref access during render)
  • WorkspacePerDiemPage.tsxreact-hooks/set-state-in-effect (setState in effect)
  • ChangeReceiptBillingAccountPage.tsxreact-hooks/set-state-in-effect (setState in effect)

Evidence: The same ESLint check is also failing on main. The Changed files ESLint check (which only lints files in this PR's diff) passes. None of the failing files are part of this PR's changes.

@stitesExpensify
Copy link
Copy Markdown
Contributor

@MelvinBot pull main again and fix the conflicts

@MelvinBot
Copy link
Copy Markdown
Contributor Author

The PR currently has merge conflicts again (mergeable state: dirty). I'd be happy to merge main and resolve the conflicts.

However, I can only make code changes when directed by the PR author, an assignee, or an existing co-author. To have me implement this, the PR assignee (huult) should reply with a request tagging me.

@stitesExpensify
Copy link
Copy Markdown
Contributor

@huult there are conflicts :/

@huult
Copy link
Copy Markdown
Contributor

huult commented Mar 23, 2026

@MelvinBot please resolve the conflict

…tReportPreviewContent

Co-authored-by: huult <huult@users.noreply.github.com>
@MelvinBot
Copy link
Copy Markdown
Contributor Author

Resolved merge conflicts with main in two files:

  1. MoneyReportHeader.tsx (3 conflicts) — Kept the PR's confirmPendingRTERAndProceed wrapper around submitReport calls (both primary and secondary submit actions), while adding the new ownerBillingGraceEndPeriod parameter from main. Also added both confirmPendingRTERAndProceed and ownerBillingGraceEndPeriod to the useCallback dependency array.

  2. MoneyRequestReportPreviewContent.tsx (1 conflict) — Same pattern: kept the confirmPendingRTERAndProceed wrapper and added the ownerBillingGraceEndPeriod parameter from main.

@stitesExpensify stitesExpensify merged commit 8aa2e67 into main Mar 24, 2026
36 checks passed
@stitesExpensify stitesExpensify deleted the claude-improveRterMovedTransactionMessage branch March 24, 2026 16:15
@github-actions
Copy link
Copy Markdown
Contributor

🚧 @stitesExpensify has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify
Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify
Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/stitesExpensify in version: 9.3.44-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

@OSBotify
Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/stitesExpensify in version: 9.3.47-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

@OSBotify
Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/grgia in version: 9.3.48-2 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants