Skip to content

Add merchant rules spreadsheet import flow#95105

Merged
NicolasBonet merged 16 commits into
mainfrom
nicolas-import-merchant-rules-spreadsheet
Jul 16, 2026
Merged

Add merchant rules spreadsheet import flow#95105
NicolasBonet merged 16 commits into
mainfrom
nicolas-import-merchant-rules-spreadsheet

Conversation

@NicolasBonet

@NicolasBonet NicolasBonet commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Adds a spreadsheet-import flow for workspace merchant rules, so admins can bulk-import rules instead of creating them one at a time. This reuses the shared import infrastructure (ImportSpreadsheet, ImportSpreadsheetColumns, useImportSpreadsheetConfirmModal) that Members/Categories/Tags/Per Diem already use.

  • Entry points: an "Import spreadsheet" menu item in the merchant rules section on the Rules page, and (behind the rulesRevamp beta) a "More" dropdown with "Import spreadsheet" next to the "Add rule" button on the Expense defaults tab.
  • New screens: ImportMerchantRulesPage (upload) and ImportedMerchantRulesPage (column mapping) under workspaces/:policyID/rules/merchant-rules/import|imported, both gated by admin + paid policy + rules feature.
  • Column roles: Merchant is, Merchant contains, Updated merchant, Updated category, Updated description, Reimbursable, Billable — with auto-matching from header text in findColumnName. Custom validation requires at least one merchant filter column plus at least one update column (new locale string for the conditional error).
  • Row → rule mapping: "Merchant is" maps to an eq filter and "Merchant contains" to a contains filter ("Merchant is" wins when both cells are filled on the same row); the description runs through Parser.replace() like setPolicyCodingRule does; Reimbursable/Billable parse true/yes/false/no and are left unset otherwise; rows without a merchant filter value are skipped.
  • API: new ImportMerchantRulesSpreadsheet side-effect command sending {policyID, rules} where rules is keyed by client-generated ruleIDs, so retried requests merge the same rules instead of duplicating them. The server result drives the shared final confirmation modal.
  • Translations: all locales; en/es reviewed by hand and the generated locales translated to match each file's existing merchant-rule terminology.

The backend command lands in companion PRs: Auth and Web-Expensify.

Holding for

https://github.com/Expensify/Web-Expensify/pull/54176

Fixed Issues

$ #88156
PROPOSAL: #88156 (comment)

Tests

  1. Log in as an admin of a Control workspace with Rules enabled.
  2. Go to Workspace settings > Rules and scroll to the Merchant rules section.
  3. Click "Import spreadsheet" and verify the upload screen opens.
  4. Upload a CSV with headers Merchant is, Merchant contains, Updated merchant, Updated category, Updated description, Reimbursable, Billable and a few data rows.
  5. Verify the columns auto-map to the matching fields and the "File contains column headers" toggle is on.
  6. Set both merchant filter columns to "(Ignore)" and click Import.
  7. Verify the error "Oops! You must map at least one "Merchant is" or "Merchant contains" column, plus at least one field to update." appears.
  8. Restore the column mapping and click Import.
  9. Verify the "Import successful" modal appears saying N merchant rules have been added.
  10. Confirm the modal and verify you land back on the Rules page with the imported rules listed in the Merchant rules section.
  11. With the rulesRevamp beta enabled, open the Expense defaults tab and verify the "More" dropdown next to "Add rule" contains "Import spreadsheet" and opens the same flow.
  • Verify that no errors appear in the JS console

Offline tests

  1. Complete the flow up to the column-mapping screen, then go offline.
  2. Click Import and verify the import stays pending (the confirmation modal waits for the server result).
  3. Go back online and verify the import completes and the rules appear in the Merchant rules section.

QA Steps

Same as tests. Note: requires the backend ImportMerchantRulesSpreadsheet command to be deployed (Auth + Web-Expensify companion PRs).

  • 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 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)
  • 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)
  • 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.
  • 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.
  • I verified that similar component doesn't exist in the codebase
  • I verified that all props are defined accurately and each prop has a /** comment above it */
  • I verified that each file is named correctly
  • I verified that each component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
  • I verified that the only data being stored in component state is data necessary for rendering and nothing else
  • In component if we are not using the full Onyx data that we loaded, I've added the proper selector in order to ensure the component only re-renders when the data it is using changes
  • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
  • I verified that component internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
  • I verified that all JSX used for rendering exists in the render method
  • I verified that each component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions

Full flow working

CleanShot.2026-07-02.at.13.06.37.mp4

With the latest change of removing the card and the option moved to the menu:

CleanShot 2026-07-08 at 09 47 25@2x

@NicolasBonet
NicolasBonet force-pushed the nicolas-import-merchant-rules-spreadsheet branch from 1dc2825 to 930cdd4 Compare July 2, 2026 16:07
@NicolasBonet NicolasBonet changed the title Add merchant rules spreadsheet import flow [Hold] Add merchant rules spreadsheet import flow Jul 2, 2026
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/CONST/index.ts 94.81% <ø> (ø)
src/SCREENS.ts 100.00% <ø> (ø)
src/libs/API/types.ts 100.00% <ø> (ø)
...gation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts 100.00% <ø> (ø)
src/libs/Navigation/linkingConfig/config.ts 76.92% <ø> (ø)
src/pages/workspace/rules/PolicyRulesPage.tsx 90.47% <100.00%> (+2.59%) ⬆️
src/ROUTES.ts 20.98% <0.00%> (-0.04%) ⬇️
...gation/AppNavigator/ModalStackNavigators/index.tsx 6.53% <0.00%> (-0.02%) ⬇️
...es/workspace/rules/getImportMerchantRulesOption.ts 28.57% <28.57%> (ø)
...rc/pages/workspace/rules/PolicyRulesPageRevamp.tsx 2.54% <0.00%> (-0.12%) ⬇️
... and 4 more
... and 13 files with indirect coverage changes

@OSBotify

This comment has been minimized.

NicolasBonet and others added 2 commits July 3, 2026 09:52
Lets workspace admins bulk-import merchant rules from a CSV/XLSX file
instead of creating them one at a time. Reuses the shared spreadsheet
import components and fires the new ImportMerchantRulesSpreadsheet
backend command with client-generated ruleIDs so retries are
idempotent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Hide empty cells in column previews instead of rendering comma runs
- Add "Updated tag" column role and import tag values into rules
- Auto-detect "Preferred Merchant (Vendor) Name", "Merchant", "Tag",
  and "Reimburseable" headers in the merchant rules context
- Skip rows with no update values and fail fast when no rules remain

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NicolasBonet
NicolasBonet force-pushed the nicolas-import-merchant-rules-spreadsheet branch from 456aa72 to 9560b29 Compare July 3, 2026 14:55
@OSBotify

This comment has been minimized.

…uage files and update references in PolicyRulesPage components to use 'importRulesTitle' instead.
@NicolasBonet NicolasBonet changed the title [Hold] Add merchant rules spreadsheet import flow Add merchant rules spreadsheet import flow Jul 8, 2026
@NicolasBonet
NicolasBonet marked this pull request as ready for review July 8, 2026 16:44
@NicolasBonet
NicolasBonet requested review from a team as code owners July 8, 2026 16:44
@melvin-bot

melvin-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown

@dubielzyk-expensify @truph01 One of you needs to 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 removed request for a team July 8, 2026 16:45
Comment thread src/pages/workspace/rules/PolicyRulesPageRevamp.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 227bfda1c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/pages/workspace/rules/MerchantRules/ImportMerchantRulesPage.tsx
…routes on rules write access

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NicolasBonet

Copy link
Copy Markdown
Contributor Author

@truph01 would it be possible to get this reviewed today, please?

@truph01

truph01 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

@truph01 would it be possible to get this reviewed today, please?

Yes, I'll review it EOD

@NicolasBonet

Copy link
Copy Markdown
Contributor Author

@truph01 let me know if you cannot check this today to pass it to another contributor please.

@NicolasBonet

Copy link
Copy Markdown
Contributor Author

@NicolasBonet What do you think about Bug 2?

100% out of scope, that is a preexisting UI that shouldn’t be modified here

@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 8638288c44b..dad8980bea1 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -1168,7 +1168,7 @@ const translations: TranslationDeepObject<typeof en> = {
             return rules > 1 ? `${rules} Händlerregeln wurden hinzugefügt.` : '1 Händlerregel wurde hinzugefügt.';
         },
         importMerchantRulesRequiredColumns:
-            'Ups! Du musst mindestens eine Spalte („Händler ist“ oder „Händler enthält“) zuordnen sowie mindestens ein zu aktualisierendes Feld. Bitte überprüfe es und versuche es erneut.',
+            'Hoppla! Sie müssen mindestens eine Spalte „Händler ist“ oder „Händler enthält“ sowie mindestens ein zu aktualisierendes Feld zuordnen. Bitte überprüfen Sie die Angaben und versuchen Sie es erneut.',
         importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
             transactions > 1 ? `${transactions} Buchungen wurden importiert.` : '1 Transaktion wurde importiert.',
         importFailedTitle: 'Import fehlgeschlagen',
@@ -7475,12 +7475,12 @@ Fordern Sie Spesendetails wie Belege und Beschreibungen an, legen Sie Limits und
                 addRuleTitle: 'Regel hinzufügen',
                 editRuleTitle: 'Regel bearbeiten',
                 importRulesTitle: 'Händlerregeln importieren',
-                importRulesSupportingText: 'Ordne jede Spalte in deiner Tabelle einem Feld der Händlerregel zu. Wenn alles gut aussieht, klicke unten, um deine Regeln zu importieren.',
+                importRulesSupportingText: 'Ordnen Sie jede Spalte in Ihrer Tabelle einem Händlerregel-Feld zu. Wenn alles gut aussieht, klicken Sie unten, um Ihre Regeln zu importieren.',
                 importColumnMerchantIs: 'Händler ist',
                 importColumnMerchantContains: 'Händler enthält',
-                importColumnUpdatedMerchant: 'Aktualisierter Händler',
-                importColumnUpdatedCategory: 'Aktualisierte Kategorie',
-                importColumnUpdatedTag: 'Aktualisiertes Tag',
+                importColumnUpdatedMerchant: 'Händler aktualisiert',
+                importColumnUpdatedCategory: 'Kategorie aktualisiert',
+                importColumnUpdatedTag: 'Tag aktualisiert',
                 importColumnUpdatedDescription: 'Aktualisierte Beschreibung',
                 expensesWith: 'Für Ausgaben mit:',
                 expensesExactlyMatching: 'Für Ausgaben mit genau folgender Übereinstimmung:',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 97b2cd29d12..3f67de62efa 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1123,12 +1123,12 @@ const translations: TranslationDeepObject<typeof en> = {
         importPerDiemRatesSuccessfulDescription: ({rates}) => (rates > 1 ? `Se han añadido ${rates} tasas de per diem.` : 'Se ha añadido 1 tasa de per diem.'),
         importMerchantRulesSuccessfulDescription: ({rules}: {rules: number}) => {
             if (rules === 0) {
-                return 'No se han añadido reglas de comerciante, ya que todas ya existen.';
+                return 'No se ha añadido ninguna regla de comerciante, ya que todas ya existen.';
             }
-            return rules > 1 ? `Se han añadido ${rules} reglas de comerciante.` : 'Se ha añadido 1 regla de comerciante.';
+            return rules > 1 ? `Se han añadido ${rules} reglas de comerciante.` : 'Se ha añadido 1 regla de comercio.';
         },
         importMerchantRulesRequiredColumns:
-            '¡Ups! Debes asignar al menos una columna "El comerciante es" o "El comerciante contiene", además de al menos un campo para actualizar. Por favor, revisa e inténtalo de nuevo.',
+            '¡Ups! Debes asignar al menos una columna de «Comerciante es» o «Comerciante contiene», además de al menos un campo para actualizar. Por favor, revisa e inténtalo de nuevo.',
         importTransactionsSuccessfulDescription: ({transactions}) => (transactions > 1 ? `Se han importado ${transactions} transacciones.` : 'Se ha importado 1 transacción.'),
         importCompanyCardTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
             transactions > 1 ? `Se han importado ${transactions} transacciones.` : 'Se ha importado 1 transacción.',
@@ -7406,10 +7406,9 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`,
                 findRule: 'Buscar regla de comerciante',
                 addRuleTitle: 'Añadir regla',
                 editRuleTitle: 'Editar regla',
-                importRulesTitle: 'Importar reglas de comerciante',
-                importRulesSupportingText:
-                    'Asigna cada columna de tu hoja de cálculo a un campo de la regla de comerciante. Cuando todo esté correcto, haz clic abajo para importar tus reglas.',
-                importColumnMerchantIs: 'El comerciante es',
+                importRulesTitle: 'Importar reglas de vendedor',
+                importRulesSupportingText: 'Asigna cada columna de tu hoja de cálculo a un campo de regla de comercio. Cuando todo se vea bien, haz clic abajo para importar tus reglas.',
+                importColumnMerchantIs: 'El comercio es',
                 importColumnMerchantContains: 'El comerciante contiene',
                 importColumnUpdatedMerchant: 'Comerciante actualizado',
                 importColumnUpdatedCategory: 'Categoría actualizada',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 7a54c8362be..62fc48f7aea 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -1167,12 +1167,12 @@ const translations: TranslationDeepObject<typeof en> = {
         importPerDiemRatesSuccessfulDescription: ({rates}: {rates: number}) => (rates > 1 ? `${rates} indemnités journalières ont été ajoutées.` : '1 taux de per diem a été ajouté.'),
         importMerchantRulesSuccessfulDescription: ({rules}: {rules: number}) => {
             if (rules === 0) {
-                return "Aucune règle de marchand n'a été ajoutée, car elles existent toutes déjà.";
+                return 'Aucune règle de marchand n’a été ajoutée, car elles existent déjà toutes.';
             }
-            return rules > 1 ? `${rules} règles de marchand ont été ajoutées.` : '1 règle de marchand a été ajoutée.';
+            return rules > 1 ? `${rules} règles de commerçant ont été ajoutées.` : '1 règle de commerçant a été ajoutée.';
         },
         importMerchantRulesRequiredColumns:
-            'Oups ! Vous devez associer au moins une colonne « Le marchand est » ou « Le marchand contient », ainsi qu’au moins un champ à mettre à jour. Veuillez examiner et réessayer.',
+            'Oups ! Vous devez mapper au moins une colonne « Merchant est » ou « Merchant contient », ainsi qu’au moins un champ à mettre à jour. Veuillez vérifier et réessayer.',
         importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
             transactions > 1 ? `${transactions} transactions ont été importées.` : '1 transaction a été importée.',
         importFailedTitle: 'Échec de l’importation',
@@ -7504,9 +7504,9 @@ Rendez obligatoires des informations de dépense comme les reçus et les descrip
                 editRuleTitle: 'Modifier la règle',
                 importRulesTitle: 'Importer des règles de marchand',
                 importRulesSupportingText:
-                    'Faites correspondre chaque colonne de votre feuille de calcul à un champ de règle de marchand. Si tout semble correct, cliquez ci-dessous pour importer vos règles.',
-                importColumnMerchantIs: 'Le marchand est',
-                importColumnMerchantContains: 'Le marchand contient',
+                    'Associez chaque colonne de votre feuille de calcul à un champ de règle de commerçant. Une fois que tout semble correct, cliquez ci-dessous pour importer vos règles.',
+                importColumnMerchantIs: 'Le commerçant est',
+                importColumnMerchantContains: 'Le commerçant contient',
                 importColumnUpdatedMerchant: 'Marchand mis à jour',
                 importColumnUpdatedCategory: 'Catégorie mise à jour',
                 importColumnUpdatedTag: 'Tag mis à jour',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index a1b10e1b816..02e2fd8b024 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -1164,11 +1164,11 @@ const translations: TranslationDeepObject<typeof en> = {
         importPerDiemRatesSuccessfulDescription: ({rates}: {rates: number}) => (rates > 1 ? `Sono state aggiunte le diarie giornaliere ${rates}.` : 'È stata aggiunta 1 diaria.'),
         importMerchantRulesSuccessfulDescription: ({rules}: {rules: number}) => {
             if (rules === 0) {
-                return 'Nessuna regola esercente è stata aggiunta, poiché esistono già tutte.';
+                return 'Non sono state aggiunte regole per gli esercenti, perché esistono già tutte.';
             }
-            return rules > 1 ? `Sono state aggiunte ${rules} regole esercente.` : 'È stata aggiunta 1 regola esercente.';
+            return rules > 1 ? `Sono state aggiunte ${rules} regole commerciante.` : 'È stata aggiunta 1 regola per l’esercente.';
         },
-        importMerchantRulesRequiredColumns: 'Ops! Devi mappare almeno una colonna “L’esercente è” o “L’esercente contiene”, oltre ad almeno un campo da aggiornare. Controlla e riprova.',
+        importMerchantRulesRequiredColumns: 'Ops! Devi mappare almeno una colonna "Merchant è" o "Merchant contiene", oltre ad almeno un campo da aggiornare. Controlla e riprova.',
         importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
             transactions > 1 ? `${transactions} transazioni sono state importate.` : '1 transazione è stata importata.',
         importFailedTitle: 'Importazione non riuscita',
@@ -7455,12 +7455,12 @@ Richiedi dettagli sulle spese come ricevute e descrizioni, imposta limiti e valo
                 addRule: 'Aggiungi regola esercente',
                 addRuleTitle: 'Aggiungi regola',
                 editRuleTitle: 'Modifica regola',
-                importRulesTitle: 'Importa regole esercente',
+                importRulesTitle: 'Importa regole esercenti',
                 importRulesSupportingText:
-                    'Mappa ogni colonna del tuo foglio di calcolo a un campo della regola esercente. Se ti sembra tutto corretto, fai clic qui sotto per importare le tue regole.',
-                importColumnMerchantIs: 'L’esercente è',
-                importColumnMerchantContains: 'L’esercente contiene',
-                importColumnUpdatedMerchant: 'Esercente aggiornato',
+                    'Associa ogni colonna del tuo foglio di calcolo a un campo della regola del fornitore. Quando è tutto corretto, fai clic qui sotto per importare le tue regole.',
+                importColumnMerchantIs: 'Il fornitore è',
+                importColumnMerchantContains: "L'esercente contiene",
+                importColumnUpdatedMerchant: 'Commerciante aggiornato',
                 importColumnUpdatedCategory: 'Categoria aggiornata',
                 importColumnUpdatedTag: 'Tag aggiornato',
                 importColumnUpdatedDescription: 'Descrizione aggiornata',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index a1b4b3eab7a..3f6df6c07bb 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -1146,12 +1146,12 @@ const translations: TranslationDeepObject<typeof en> = {
         importPerDiemRatesSuccessfulDescription: ({rates}: {rates: number}) => (rates > 1 ? `${rates}件の日当レートが追加されました。` : '1件の日当レートが追加されました。'),
         importMerchantRulesSuccessfulDescription: ({rules}: {rules: number}) => {
             if (rules === 0) {
-                return 'すべての加盟店ルールが既に存在するため、追加されませんでした。';
+                return 'すべてのルールがすでに存在するため、マーチャントルールは追加されていません。';
             }
-            return rules > 1 ? `${rules}件の加盟店ルールが追加されました。` : '1件の加盟店ルールが追加されました。';
+            return rules > 1 ? `${rules} 件のマーチャントルールを追加しました。` : 'マーチャントルールが1件追加されました。';
         },
         importMerchantRulesRequiredColumns:
-            'おっと!「加盟店が一致」または「加盟店を含む」の列を少なくとも1つ、さらに更新するフィールドを少なくとも1つマッピングしてください。確認してもう一度お試しください。',
+            'エラーが発生しました。「Merchant is」または「Merchant contains」列のうち少なくとも1つと、更新するフィールドを少なくとも1つ対応付ける必要があります。内容を確認してもう一度お試しください。',
         importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
             transactions > 1 ? `${transactions} 件の取引がインポートされました。` : '1件の取引がインポートされました。',
         importFailedTitle: 'インポートに失敗しました',
@@ -7365,14 +7365,15 @@ ${reportName}`,
                 addRule: '支払先ルールを追加',
                 addRuleTitle: 'ルールを追加',
                 editRuleTitle: 'ルールを編集',
-                importRulesTitle: '加盟店ルールをインポート',
-                importRulesSupportingText: 'スプレッドシートの各列を加盟店ルールのフィールドに対応付けてください。問題なければ、下をクリックしてルールをインポートしてください。',
-                importColumnMerchantIs: '加盟店が一致',
-                importColumnMerchantContains: '加盟店を含む',
-                importColumnUpdatedMerchant: '更新後の加盟店',
-                importColumnUpdatedCategory: '更新後のカテゴリ',
-                importColumnUpdatedTag: '更新後のタグ',
-                importColumnUpdatedDescription: '更新後の説明',
+                importRulesTitle: 'マーチャントルールをインポート',
+                importRulesSupportingText:
+                    'スプレッドシート内の各列を、マーチャントルールの項目に対応付けてください。すべて問題ないことを確認したら、下のボタンをクリックしてルールをインポートします。',
+                importColumnMerchantIs: '加盟店:',
+                importColumnMerchantContains: '支払先に次を含む',
+                importColumnUpdatedMerchant: '更新された支払先',
+                importColumnUpdatedCategory: 'カテゴリを更新しました',
+                importColumnUpdatedTag: 'タグを更新しました',
+                importColumnUpdatedDescription: '更新された説明',
                 expensesWith: '対象となる経費条件:',
                 expensesExactlyMatching: '次の条件に完全一致する経費の場合:',
                 applyUpdates: 'これらの更新を適用する',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 368a79a6bfa..6c3e9634533 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -1162,12 +1162,12 @@ const translations: TranslationDeepObject<typeof en> = {
         importPerDiemRatesSuccessfulDescription: ({rates}: {rates: number}) => (rates > 1 ? `${rates} per diem-tarieven zijn toegevoegd.` : '1 dagvergoeding is toegevoegd.'),
         importMerchantRulesSuccessfulDescription: ({rules}: {rules: number}) => {
             if (rules === 0) {
-                return 'Er zijn geen handelaarregels toegevoegd, omdat ze allemaal al bestaan.';
+                return 'Er zijn geen leverancierregels toegevoegd, omdat ze allemaal al bestaan.';
             }
-            return rules > 1 ? `${rules} handelaarregels zijn toegevoegd.` : '1 handelaarregel is toegevoegd.';
+            return rules > 1 ? `${rules} handelaarsregels zijn toegevoegd.` : 'Er is 1 leveranciersregel toegevoegd.';
         },
         importMerchantRulesRequiredColumns:
-            'Oeps! Je moet ten minste één kolom "Handelaar is" of "Handelaar bevat" toewijzen, plus ten minste één veld om bij te werken. Controleer het en probeer het opnieuw.',
+            'Oeps! Je moet minstens één kolom “Handelaar is” of “Handelaar bevat” koppelen, plus minstens één veld om bij te werken. Controleer alles en probeer het opnieuw.',
         importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
             transactions > 1 ? `${transactions} transacties zijn geïmporteerd.` : '1 transactie is geïmporteerd.',
         importFailedTitle: 'Import mislukt',
@@ -7436,14 +7436,14 @@ Vereis onkostendetails zoals bonnen en beschrijvingen, stel limieten en standaar
                 addRule: 'Merchantregel toevoegen',
                 addRuleTitle: 'Regel toevoegen',
                 editRuleTitle: 'Regel bewerken',
-                importRulesTitle: 'Handelaarregels importeren',
-                importRulesSupportingText: 'Koppel elke kolom in je spreadsheet aan een veld van een handelaarregel. Als alles er goed uitziet, klik hieronder om je regels te importeren.',
-                importColumnMerchantIs: 'Handelaar is',
+                importRulesTitle: 'Merchantregels importeren',
+                importRulesSupportingText: 'Koppel elke kolom in je spreadsheet aan een veld voor handelsregels. Zodra alles er goed uitziet, klik je hieronder om je regels te importeren.',
+                importColumnMerchantIs: 'Winkel is',
                 importColumnMerchantContains: 'Handelaar bevat',
-                importColumnUpdatedMerchant: 'Bijgewerkte handelaar',
-                importColumnUpdatedCategory: 'Bijgewerkte categorie',
-                importColumnUpdatedTag: 'Bijgewerkte tag',
-                importColumnUpdatedDescription: 'Bijgewerkte omschrijving',
+                importColumnUpdatedMerchant: 'Bijgewerkte leverancier',
+                importColumnUpdatedCategory: 'Categorie bijgewerkt',
+                importColumnUpdatedTag: 'Tag bijgewerkt',
+                importColumnUpdatedDescription: 'Beschrijving bijgewerkt',
                 expensesWith: 'Voor declaraties met:',
                 expensesExactlyMatching: 'Voor onkosten die exact overeenkomen met:',
                 applyUpdates: 'Pas deze updates toe:',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index fe28ee1c3c7..e790bd9b2e8 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -1161,10 +1161,10 @@ const translations: TranslationDeepObject<typeof en> = {
             if (rules === 0) {
                 return 'Nie dodano żadnych reguł sprzedawcy, ponieważ wszystkie już istnieją.';
             }
-            return rules > 1 ? `Dodano reguły sprzedawcy: ${rules}.` : 'Dodano 1 regułę sprzedawcy.';
+            return rules > 1 ? `Dodano ${rules} reguły handlowca.` : 'Dodano 1 regułę dla sprzedawcy.';
         },
         importMerchantRulesRequiredColumns:
-            'Ups! Musisz zmapować co najmniej jedną kolumnę „Sprzedawca to” lub „Sprzedawca zawiera” oraz co najmniej jedno pole do aktualizacji. Sprawdź i spróbuj ponownie.',
+            'Ups! Musisz przypisać co najmniej jedną kolumnę „Merchant is” lub „Merchant contains” oraz co najmniej jedno pole do zaktualizowania. Sprawdź ustawienia i spróbuj ponownie.',
         importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) => (transactions > 1 ? `Zaimportowano ${transactions} transakcje.` : 'Zaimportowano 1 transakcję.'),
         importFailedTitle: 'Import nieudany',
         importFailedDescription: 'Upewnij się, że wszystkie pola zostały poprawnie wypełnione i spróbuj ponownie. Jeśli problem będzie się powtarzał, skontaktuj się z Concierge.',
@@ -7415,15 +7415,15 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i
                 addRule: 'Dodaj regułę dla sprzedawcy',
                 addRuleTitle: 'Dodaj regułę',
                 editRuleTitle: 'Edytuj regułę',
-                importRulesTitle: 'Importuj reguły sprzedawcy',
+                importRulesTitle: 'Importuj reguły sprzedawców',
                 importRulesSupportingText:
-                    'Zmapuj każdą kolumnę w arkuszu kalkulacyjnym do pola reguły sprzedawcy. Jeśli wszystko wygląda dobrze, kliknij poniżej, aby zaimportować swoje reguły.',
+                    'Przypisz każdą kolumnę w swoim arkuszu kalkulacyjnym do pola reguły sprzedawcy. Gdy wszystko będzie wyglądać dobrze, kliknij poniżej, aby zaimportować swoje reguły.',
                 importColumnMerchantIs: 'Sprzedawca to',
                 importColumnMerchantContains: 'Sprzedawca zawiera',
-                importColumnUpdatedMerchant: 'Zaktualizowany sprzedawca',
-                importColumnUpdatedCategory: 'Zaktualizowana kategoria',
-                importColumnUpdatedTag: 'Zaktualizowany tag',
-                importColumnUpdatedDescription: 'Zaktualizowany opis',
+                importColumnUpdatedMerchant: 'Zaktualizowano sprzedawcę',
+                importColumnUpdatedCategory: 'Zaktualizowano kategorię',
+                importColumnUpdatedTag: 'Zaktualizowano znacznik',
+                importColumnUpdatedDescription: 'Zaktualizowano opis',
                 expensesWith: 'Dla wydatków z:',
                 expensesExactlyMatching: 'Dla wydatków dokładnie pasujących do:',
                 applyUpdates: 'Zastosuj te aktualizacje:',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 5c5acc3c28d..74912587faa 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -1162,12 +1162,12 @@ const translations: TranslationDeepObject<typeof en> = {
         importPerDiemRatesSuccessfulDescription: ({rates}: {rates: number}) => (rates > 1 ? `Foram adicionadas ${rates} diárias.` : '1 diária foi adicionada.'),
         importMerchantRulesSuccessfulDescription: ({rules}: {rules: number}) => {
             if (rules === 0) {
-                return 'Nenhuma regra de comerciante foi adicionada, pois todas já existem.';
+                return 'Nenhuma regra de comerciante foi adicionada, pois todas elas já existem.';
             }
-            return rules > 1 ? `Foram adicionadas ${rules} regras de comerciante.` : '1 regra de comerciante foi adicionada.';
+            return rules > 1 ? `${rules} regras de comerciante foram adicionadas.` : '1 regra de comerciante foi adicionada.';
         },
         importMerchantRulesRequiredColumns:
-            'Ops! Você precisa mapear pelo menos uma coluna "O comerciante é" ou "O comerciante contém", além de pelo menos um campo para atualizar. Revise e tente novamente.',
+            'Ops! Você deve mapear pelo menos uma coluna de "Merchant é" ou "Merchant contém", além de pelo menos um campo para atualizar. Revise e tente novamente.',
         importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) =>
             transactions > 1 ? `${transactions} transações foram importadas.` : '1 transação foi importada.',
         importFailedTitle: 'Falha na importação',
@@ -7428,10 +7428,10 @@ Exija dados de despesas como recibos e descrições, defina limites e padrões e
                 addRule: 'Adicionar regra de comerciante',
                 addRuleTitle: 'Adicionar regra',
                 editRuleTitle: 'Editar regra',
-                importRulesTitle: 'Importar regras de comerciante',
-                importRulesSupportingText: 'Mapeie cada coluna da sua planilha para um campo de regra de comerciante. Se tudo estiver certo, clique abaixo para importar suas regras.',
-                importColumnMerchantIs: 'O comerciante é',
-                importColumnMerchantContains: 'O comerciante contém',
+                importRulesTitle: 'Importar regras de estabelecimento',
+                importRulesSupportingText: 'Mapeie cada coluna da sua planilha para um campo de regra de comerciante. Quando tudo estiver certo, clique abaixo para importar suas regras.',
+                importColumnMerchantIs: 'Estabelecimento é',
+                importColumnMerchantContains: 'Estabelecimento contém',
                 importColumnUpdatedMerchant: 'Comerciante atualizado',
                 importColumnUpdatedCategory: 'Categoria atualizada',
                 importColumnUpdatedTag: 'Tag atualizada',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index f165fa686c2..1747313a9eb 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -1118,11 +1118,11 @@ const translations: TranslationDeepObject<typeof en> = {
         importPerDiemRatesSuccessfulDescription: ({rates}: {rates: number}) => (rates > 1 ? `已添加 ${rates} 个每日补贴标准。` : '已添加 1 条日津贴费率。'),
         importMerchantRulesSuccessfulDescription: ({rules}: {rules: number}) => {
             if (rules === 0) {
-                return '未添加任何商户规则,因为它们均已存在。';
+                return '尚未添加商家规则,因为它们都已存在。';
             }
-            return rules > 1 ? `已添加 ${rules} 条商户规则。` : '已添加 1 条商户规则。';
+            return rules > 1 ? `已添加 ${rules} 条商家规则。` : '已添加 1 条商户规则。';
         },
-        importMerchantRulesRequiredColumns: '哎呀!您必须至少映射一列“商户为”或“商户包含”,并且至少映射一个要更新的字段。请检查后重试。',
+        importMerchantRulesRequiredColumns: '哎呀!您必须至少映射一列“商户为”或“商户包含”,并且至少选择一个要更新的字段。请检查后重试。',
         importTransactionsSuccessfulDescription: ({transactions}: {transactions: number}) => (transactions > 1 ? `已导入 ${transactions} 笔交易。` : '已导入 1 笔交易。'),
         importFailedTitle: '导入失败',
         importFailedDescription: '请确保所有字段均已正确填写,然后重试。如果问题仍然存在,请联系 Concierge。',
@@ -7214,14 +7214,14 @@ ${reportName}`,
                 addRule: '添加商家规则',
                 addRuleTitle: '添加规则',
                 editRuleTitle: '编辑规则',
-                importRulesTitle: '导入商户规则',
-                importRulesSupportingText: '将电子表格中的每一列映射到商户规则的字段。如果一切看起来都没问题,请点击下方以导入您的规则。',
+                importRulesTitle: '导入商家规则',
+                importRulesSupportingText: '将电子表格中的每一列映射到商户规则字段。确认无误后,点击下方按钮导入规则。',
                 importColumnMerchantIs: '商户为',
                 importColumnMerchantContains: '商户包含',
-                importColumnUpdatedMerchant: '更新后的商户',
-                importColumnUpdatedCategory: '更新后的类别',
-                importColumnUpdatedTag: '更新后的标签',
-                importColumnUpdatedDescription: '更新后的描述',
+                importColumnUpdatedMerchant: '已更新商户',
+                importColumnUpdatedCategory: '已更新类别',
+                importColumnUpdatedTag: '已更新的标签',
+                importColumnUpdatedDescription: '已更新描述',
                 expensesWith: '针对以下费用:',
                 expensesExactlyMatching: '对于完全匹配以下条件的报销:',
                 applyUpdates: '应用这些更新:',

Note

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

View workflow run

@NicolasBonet

Copy link
Copy Markdown
Contributor Author

@truph01 this has been open for quite sometime, let's prioritize its review please 🙏

@truph01

truph01 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible 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 checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (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 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)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • 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 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.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this 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.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Screen.Recording.2026-07-15.at.06.58.18.mov
Android: mWeb Chrome
iOS: HybridApp
Screen.Recording.2026-07-15.at.07.06.50.mov
iOS: mWeb Safari
Screen.Recording.2026-07-15.at.07.04.57.mov
MacOS: Chrome / Safari
Screen.Recording.2026-07-15.at.06.53.25.mov

@melvin-bot
melvin-bot Bot requested a review from youssef-lr July 15, 2026 00:08
@melvin-bot

melvin-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

@youssef-lr 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]

@truph01

truph01 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@youssef-lr All yours!

@NicolasBonet
NicolasBonet merged commit 1575acd into main Jul 16, 2026
45 checks passed
@NicolasBonet
NicolasBonet deleted the nicolas-import-merchant-rules-spreadsheet branch July 16, 2026 21:39
@github-actions

Copy link
Copy Markdown
Contributor

🚧 NicolasBonet 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/NicolasBonet in version: 9.4.36-0 🚀

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

@MelvinBot

Copy link
Copy Markdown
Contributor

🤖 I reviewed the changes in this PR and help site updates are required.

This PR adds a spreadsheet import flow for Merchant Rules (bulk-import via More > Import merchant rules on the Rules page), which the existing Merchant Rules article didn't cover. I've drafted the docs update.

Draft help site PR: #96359

It adds a How to import Merchant Rules using a spreadsheet section to Workspace-Merchant-Rules.md covering the entry point, file upload, column mapping (Merchant is / Merchant contains / Updated merchant / Updated category / Updated tag / Updated description / Reimbursable / Billable), the mapping requirements, and the no-duplicate behavior. All UI labels were verified against the current App code and follow the HelpDot authoring guidelines, naming conventions, and template.

@MelvinBot, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR Ready for review.

@kavimuru

Copy link
Copy Markdown

@NicolasBonet can you please share the Csv file?

@truph01

truph01 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

@kavimuru You can try it #88156 (comment)

@mitarachim

Copy link
Copy Markdown

Deploy Blocker #96389 was identified to be related to this PR.

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #96391 was identified to be related to this PR.

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.

8 participants