Vendor matching CC - R1: App Phase 3 (expense flow UI)#91886
Draft
Beamanator wants to merge 4 commits into
Draft
Vendor matching CC - R1: App Phase 3 (expense flow UI)#91886Beamanator wants to merge 4 commits into
Beamanator wants to merge 4 commits into
Conversation
New vendor selector RHP that lists QBO vendors with name search and writes the user pick via the Phase 1 updateMoneyRequestVendor action (isManuallySet hardcoded true). Wires the screen, route, navigation type, linking config, modal-stack registration, withWritableReportOrNotFound + withFullTransactionOrNotFound union members, and adds the common.vendor translation in all 10 supported locales.
Surfaces the current expense vendor below the Category row when the
workspace has the vendorMatching beta + QBO Credit/Debit-card export
config AND the expense is non-reimbursable. Tapping opens the new
MONEY_REQUEST_STEP_VENDOR RHP. The inactiveVendor violation is surfaced
via getErrorForField('vendor') using the Phase 1 useViolations mapping
(inactiveVendor → 'vendor' field).
Wires the App side of the Phase 1 Auth-emitted CONCIERGEAUTOMATCHVENDOR report action: new CONST entry, OriginalMessage type (vendorName + reasoning), renderer using ReportActionItemMessageWithExplain (Explain link surfaces automatically via hasReasoning check), and translations across all 10 supported locales.
Removes getUrlWithBackToParam usage from the new route (the deprecated backTo pattern, per contributingGuides/NAVIGATION.md) and removes the unused CONST import in ConciergeAutoMatchVendorContent. Navigation back from the RHP just closes the modal, returning to the previous view.
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 diffdiff --git a/src/languages/de.ts b/src/languages/de.ts
index e9dbd702836..3dfceb92d8b 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -314,7 +314,7 @@ const translations: TranslationDeepObject<typeof en> = {
merchant: 'Händler',
change: 'Ändern',
category: 'Kategorie',
- vendor: 'Anbieter',
+ vendor: 'Lieferant',
report: 'Bericht',
billable: 'Abrechenbar',
nonBillable: 'Nicht abrechenbar',
@@ -1657,7 +1657,7 @@ const translations: TranslationDeepObject<typeof en> = {
},
correctRateError: 'Beheben Sie den Kursfehler und versuchen Sie es erneut.',
AskToExplain: `. <a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}">Erklären<sparkles-icon/></a>`,
- conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge hat diese Ausgabe <strong>${vendorName}</strong> zugeordnet`,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge hat diese Ausgabe mit <strong>${vendorName}</strong> abgeglichen`,
duplicateNonDefaultWorkspacePerDiemError:
'Sie können Per-Diem-Ausgaben nicht über mehrere Workspaces hinweg duplizieren, da sich die Sätze zwischen den Workspaces unterscheiden können.',
rulesModifiedFields: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 6257665a4a8..2b86e605c9c 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1620,7 +1620,7 @@ const translations: TranslationDeepObject<typeof en> = {
},
correctRateError: 'Corrige el error de la tasa y vuelve a intentarlo.',
AskToExplain: `. <a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}">Explicar<sparkles-icon/></a>`,
- conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge asoció este gasto con <strong>${vendorName}</strong>`,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge vinculó este gasto con <strong>${vendorName}</strong>`,
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? 'marcó el gasto como "reembolsable"' : 'marcó el gasto como "no reembolsable"'),
billable: (value: boolean) => (value ? 'marcó el gasto como "facturable"' : 'marcó el gasto como "no facturable"'),
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 5d630887a5b..a21a491cca3 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -1663,7 +1663,7 @@ const translations: TranslationDeepObject<typeof en> = {
},
correctRateError: 'Corrigez l’erreur de taux et réessayez.',
AskToExplain: `. <a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}">Expliquer<sparkles-icon/></a>`,
- conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge a associé cette dépense au fournisseur <strong>${vendorName}</strong>`,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge a fait correspondre cette dépense à <strong>${vendorName}</strong>`,
duplicateNonDefaultWorkspacePerDiemError:
'Vous ne pouvez pas dupliquer les indemnités journalières entre plusieurs espaces de travail, car les taux peuvent différer d’un espace de travail à l’autre.',
rulesModifiedFields: {
diff --git a/src/languages/it.ts b/src/languages/it.ts
index f4336fa77d4..f379a8f5e47 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -1656,7 +1656,7 @@ const translations: TranslationDeepObject<typeof en> = {
},
correctRateError: "Correggi l'errore di tariffa e riprova.",
AskToExplain: `. <a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}">Spiega<sparkles-icon/></a>`,
- conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge ha associato questa spesa al fornitore <strong>${vendorName}</strong>`,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge ha abbinato questa spesa a <strong>${vendorName}</strong>`,
duplicateNonDefaultWorkspacePerDiemError: 'Non puoi duplicare le spese di diaria tra diversi spazi di lavoro perché le tariffe potrebbero essere diverse tra gli spazi di lavoro.',
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? 'ha contrassegnato la spesa come "rimborsabile"' : 'ha contrassegnato la spesa come "non rimborsabile"'),
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index ba954497372..faf0014237d 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -314,7 +314,7 @@ const translations: TranslationDeepObject<typeof en> = {
merchant: '加盟店',
change: '変更',
category: 'カテゴリ',
- vendor: 'ベンダー',
+ vendor: '仕入先',
report: 'レポート',
billable: '請求可能',
nonBillable: '請求不可',
@@ -1637,7 +1637,7 @@ const translations: TranslationDeepObject<typeof en> = {
},
correctRateError: 'レートのエラーを修正して、もう一度お試しください。',
AskToExplain: `・<a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}">説明<sparkles-icon/></a>`,
- conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge がこの経費を <strong>${vendorName}</strong> に一致させました`,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge がこの経費を<strong>${vendorName}</strong>にマッチしました`,
duplicateNonDefaultWorkspacePerDiemError: 'ワークスペースごとに日当レートが異なる場合があるため、日当経費をワークスペース間で複製することはできません。',
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? '経費を「精算対象」に指定しました' : '経費を「精算対象外」にマークしました'),
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index d005d8705aa..80b1356b258 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -1650,7 +1650,7 @@ const translations: TranslationDeepObject<typeof en> = {
},
correctRateError: 'Corrija o erro de taxa e tente novamente.',
AskToExplain: `. <a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}">Explicar<sparkles-icon/></a>`,
- conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge associou esta despesa ao fornecedor <strong>${vendorName}</strong>`,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge associou essa despesa a <strong>${vendorName}</strong>`,
duplicateNonDefaultWorkspacePerDiemError: 'Você não pode duplicar despesas de diárias entre espaços de trabalho porque as tarifas podem variar entre eles.',
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? 'marcou a despesa como "reembolsável"' : 'marcou a despesa como “não reembolsável”'),
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 141c6e4c991..d96fd62801f 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -1602,7 +1602,7 @@ const translations: TranslationDeepObject<typeof en> = {
},
correctRateError: '修复费率错误后请重试。',
AskToExplain: `。<a href="${CONST.CONCIERGE_EXPLAIN_LINK_PATH}">说明<sparkles-icon/></a>`,
- conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge 已将此支出匹配到<strong>${vendorName}</strong>`,
+ conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge 已将此报销与 <strong>${vendorName}</strong> 匹配`,
duplicateNonDefaultWorkspacePerDiemError: '您无法在不同工作区之间复制每日津贴报销,因为各工作区的补贴标准可能不同。',
rulesModifiedFields: {
reimbursable: (value: boolean) => (value ? '将该报销单标记为“可报销”' : '将该报销单标记为“不可报销”'),
Note You can apply these changes to your branch by copying the patch to your clipboard, then running |
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.
|
51 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Explanation of Change
Phase 3 of Vendor matching CC - R1: App — the user-facing expense-flow UI. With this PR the end-to-end R1 flow is in place: an admin enables the
vendorMatchingbeta on a QBO + Credit/Debit-card workspace, the PHP fuzzy matcher (already on staging via the Web-Expensify commands and event hooks PR) writescomment.vendoron incoming non-reimbursable expenses, and submitters/approvers can see + manually override the matched vendor on every expense.Four chunks, four commits:
IOURequestStepVendorRHP + nav wiring — new searchable vendor selector. Builds the list fromgetQBOVendors(policy)(Phase 1 helper), filters by name, and on selection callsupdateMoneyRequestVendor(transactionID, vendorID, transaction)— the Phase 1 action that handles the optimisticcomment.vendorwrite + the optimisticinactiveVendorviolation clear. Adds the standard nav surface:SCREENS.MONEY_REQUEST.STEP_VENDOR,ROUTES.MONEY_REQUEST_STEP_VENDOR, the param type, linking config, modal-stack entry, and the screen-name union in bothwithWritableReportOrNotFoundandwithFullTransactionOrNotFound. Newcommon.vendortranslation in all 10 supported locales.Vendor row on
MoneyRequestView— surfaces the current vendor below the Category row.shouldShowVendor = hasVendorFeature(policy, isBetaEnabled(BETAS.VENDOR_MATCHING)) && !isCurrentTransactionReimbursable, so the row only appears on non-reimbursable expenses for workspaces with the beta + QBO Credit/Debit-card config. Title resolves viagetQBOVendorByID(policy, transaction?.comment?.vendor?.externalID)?.name— empty when the matched vendor has since been removed from QBO. Tapping the row opens the new RHP. TheinactiveVendorviolation auto-surfaces viagetErrorForField('vendor')because Phase 1 already mappedinactiveVendor → 'vendor'inuseViolations.ts.CONCIERGEAUTOMATCHVENDORaction rendering — new App-side surface for the Concierge system message that Phase 1 Auth emits when the PHP matcher writes an auto-match. AddsCONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_MATCH_VENDOR, theOriginalMessageConciergeAutoMatchVendortype (vendorNamefor display +reasoningfor the LLM-consumable explanation), a newConciergeAutoMatchVendorContentrenderer, and the route entry inActionContentRouter. ReusesReportActionItemMessageWithExplain, which automatically appends the "Explain" link whenever the action's originalMessage has areasoningfield — clicking opens a Concierge thread that explains the match. Newiou.conciergeAutoMatchedVendortranslation in all 10 locales.Cleanup — drops the deprecated
getUrlWithBackToParamfrom the new route percontributingGuides/NAVIGATION.md(would have tripped the eslint-seatbelt baseline by 1); removes an unused CONST import.Phase 1 / Phase 2 dependencies that are already merged
updateMoneyRequestVendoraction,BETAS.VENDOR_MATCHINGgate,inactiveVendorviolation client-compute. Phase 3 is a thin layer over the Phase 1 surface.vendorNVP +UpdateMoneyRequestVendorAuth command +ACTION_CONCIERGE_AUTO_MATCH_VENDORconstant.What's NOT in this PR (intentional Phase 4 / R2 work)
comment.vendor.externalIDinstead of merchant-string matching — Integration-Server has aBetaManagerbut it gates on hardcoded policy IDs, not workspacebetas[]. We're shipping Java as a clean unconditional cutover at R1 GA rather than introducing a parallel hardcoded-IDs path.pushTransactionViolationsOnyxDatacallers on QBO export-type change + vendor-list sync (deferred from Phase 2) — still requires either refactoring out ofupdateManyPolicyConnectionConfigsor a new Onyx subscriber pattern. Zero workspaces on the beta today; violations recompute lazily on next transaction edit.Fixed Issues
$ https://github.com/Expensify/Expensify/issues/638653
PROPOSAL: N/A (internal feature, no external proposal)
Tests
Prerequisite: a workspace with the
vendorMatchingbeta enabled, QBO connected, and non-reimbursable export destination set to Credit Card or Debit Card. The PHP fuzzy matcher needs to be on the environment for the auto-match path; the manual-pick path works regardless.Manual vendor pick path:
vendorMatchingbeta disabled on the workspace, verify the Vendor row is hidden.Auto-match path (requires the Web-Expensify PHP fuzzy matcher PR on the environment + vendorMatching beta opted in):
ACME*1234againstAcme Corp).reasoningfield on the action's originalMessage).Acme Corp.Inactive vendor violation path:
Offline tests
Same as Tests, executed with the network disabled after step 1. Verify the manual-pick flow surfaces the standard
OfflineWithFeedbackpending-action indicator on the Vendor row, and that the optimistic vendor update (and optimisticinactiveVendorviolation clear) from Phase 1's action persist correctly when the connection returns.QA Steps
Same as Tests, but on staging with the
vendorMatchingbeta enabled. Internal QA only — feature is gated by beta opt-in and only meaningful on workspaces with QBO + CC/DC export.PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps./** comment above it */thisproperly so there are no scoping issues (i.e. foronClick={this.submit}the methodthis.submitshould be bound tothisin the constructor)thisare necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);ifthis.submitis never passed to a component event handler likeonClick)Screenshots/Videos
To be added once a local build is up. The new UI surfaces are: the Vendor row on MoneyRequestView, the IOURequestStepVendor RHP, and the ConciergeAutoMatchVendor system message in the transaction thread.
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari