From 43349abda8eab2dd35b515999e6bddfd0955ab11 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Thu, 13 Aug 2026 15:32:36 -0600 Subject: [PATCH 1/2] Distinguish re-authentication scrape codes (438/532) in RTER violation copy --- src/CONST/index.ts | 1 + .../BrokenConnectionDescription.tsx | 13 ++++++++- src/components/MoneyRequestHeader.tsx | 5 +++- src/languages/de.ts | 16 +++++++++++ src/languages/el.ts | 17 ++++++++++++ src/languages/en.ts | 16 +++++++++++ src/languages/es.ts | 17 ++++++++++++ src/languages/fr.ts | 17 ++++++++++++ src/languages/it.ts | 17 ++++++++++++ src/languages/ja.ts | 16 +++++++++++ src/languages/nl.ts | 16 +++++++++++ src/languages/pl.ts | 16 +++++++++++ src/languages/pt-BR.ts | 16 +++++++++++ src/languages/zh-hans.ts | 14 ++++++++++ src/libs/TransactionUtils/index.ts | 7 +++-- tests/unit/TransactionUtilsTest.ts | 26 ++++++++++++++++++ tests/unit/ViolationUtilsTest.ts | 27 +++++++++++++++++++ 17 files changed, 253 insertions(+), 4 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index d6546d0c8370..3d904dc4fa66 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6561,6 +6561,7 @@ const CONST = { RTER_VIOLATION_TYPES: { BROKEN_CARD_CONNECTION: 'brokenCardConnection', BROKEN_CARD_CONNECTION_530: 'brokenCardConnection530', + BROKEN_CARD_CONNECTION_REAUTH: 'brokenCardConnectionReauth', SEVEN_DAY_HOLD: 'sevenDayHold', }, REVIEW_DUPLICATES_ORDER: ['merchant', 'category', 'tag', 'description', 'taxCode', 'billable', 'reimbursable'], diff --git a/src/components/BrokenConnectionDescription.tsx b/src/components/BrokenConnectionDescription.tsx index 6699ddc1b190..cd8b9ba2a781 100644 --- a/src/components/BrokenConnectionDescription.tsx +++ b/src/components/BrokenConnectionDescription.tsx @@ -32,11 +32,12 @@ function BrokenConnectionDescription({transactionID, policy, report}: BrokenConn const {environmentURL} = useEnvironment(); const brokenConnection530Error = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530); + const brokenConnectionReauthError = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH); const brokenConnectionError = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION); const isPolicyAdmin = isPolicyAdminPolicyUtils(policy); const workspaceCompanyCardRoute = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policy?.id)}`; - if (!brokenConnection530Error && !brokenConnectionError) { + if (!brokenConnection530Error && !brokenConnectionReauthError && !brokenConnectionError) { return ''; } @@ -44,6 +45,16 @@ function BrokenConnectionDescription({transactionID, policy, report}: BrokenConn return translate('violations.brokenConnection530Error'); } + if (brokenConnectionReauthError) { + if (isPolicyAdmin && !isCurrentUserSubmitter(report)) { + return ; + } + if (isReportApproved({report}) || isReportManuallyReimbursed(report)) { + return translate('violations.memberReauthConnectionError'); + } + return `${translate('violations.memberReauthConnectionError')} ${translate('violations.markAsCashToIgnore')}`; + } + if (isPolicyAdmin && !isCurrentUserSubmitter(report)) { return ; } diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 0dadca3f4edf..a66a68d23194 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -128,7 +128,10 @@ function MoneyRequestHeader({reportID: reportIDProp, onBackButtonPress}: MoneyRe return {icon: getStatusIcon(icons.CreditCardHourglass), description: translate('iou.transactionPendingDescription')}; } if (!!transaction?.transactionID && !!transactionViolations.length && shouldShowBrokenConnectionViolation) { - const brokenConnectionError = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION); + const brokenConnectionError = transactionViolations?.find( + (violation) => + violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH, + ); const cardID = brokenConnectionError?.data?.cardID; const card = cardID ? cardList?.[cardID] : undefined; const isBrokenPersonalCard = isPersonalCard(card); diff --git a/src/languages/de.ts b/src/languages/de.ts index 9dc76a1e4123..f6b56c4b9eb3 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -9803,6 +9803,19 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Beleg kann wegen unterbrochener Bankverbindung nicht automatisch zugeordnet werden.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH) { + if (isPersonalCard) { + if (!connectionLink) { + return 'Beleg kann nicht automatisch zugeordnet werden, da deine Bankverbindung erneut authentifiziert werden muss.'; + } + return isMarkAsCash + ? `Beleg kann nicht automatisch zugeordnet werden, da deine Bankverbindung erneut authentifiziert werden muss. Markiere ihn als Barzahlung, um ihn zu ignorieren, oder verbinde dich erneut, um den Beleg zuzuordnen.` + : `Beleg kann nicht automatisch zugeordnet werden, da deine Bankverbindung erneut authentifiziert werden muss. Erneut verbinden, um den Beleg zuzuordnen.`; + } + return isAdmin + ? `Bankverbindung muss erneut authentifiziert werden. Erneut verbinden, um Beleg zuzuordnen` + : 'Bankverbindung muss erneut authentifiziert werden. Bitte eine:n Admin bitten, die Verbindung wiederherzustellen, um den Beleg abzugleichen.'; + } if (isPersonalCard && (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || brokenBankConnection)) { if (!connectionLink) { return 'Beleg kann wegen unterbrochener Bankverbindung nicht automatisch zugeordnet werden.'; @@ -9825,6 +9838,9 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc adminBrokenConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => `Beleg ausstehend wegen unterbrochener Bankverbindung. Bitte in Firmenkarten beheben.`, memberBrokenConnectionError: 'Beleg ausstehend aufgrund einer unterbrochenen Bankverbindung. Bitte wende dich an eine Workspace-Admin, um das Problem zu beheben.', + adminReauthConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => + `Beleg ausstehend, da die Bankverbindung erneut authentifiziert werden muss. Bitte in Firmenkarten beheben.`, + memberReauthConnectionError: 'Beleg ausstehend, da die Bankverbindung erneut authentifiziert werden muss. Bitte wende dich an eine Workspace-Admin, um das Problem zu beheben.', markAsCashToIgnore: 'Als Barzahlung markieren, um sie zu ignorieren und Zahlung anzufordern.', smartscanFailed: ({canEdit = true, missingFields = []}: {canEdit?: boolean; missingFields?: string[]}) => { if (missingFields.length > 0) { diff --git a/src/languages/el.ts b/src/languages/el.ts index 6a92a3741f69..550d00f0a4b6 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -10034,6 +10034,19 @@ ${reportName}`, if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Δεν είναι δυνατή η αυτόματη αντιστοίχιση της απόδειξης λόγω σπασμένης σύνδεσης με την τράπεζα.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH) { + if (isPersonalCard) { + if (!connectionLink) { + return 'Δεν είναι δυνατή η αυτόματη αντιστοίχιση της απόδειξης, επειδή η σύνδεση με την τράπεζα χρειάζεται εκ νέου ταυτοποίηση.'; + } + return isMarkAsCash + ? `Δεν είναι δυνατή η αυτόματη αντιστοίχιση της απόδειξης, επειδή η σύνδεση με την τράπεζα χρειάζεται εκ νέου ταυτοποίηση. Σημειώστε την ως μετρητά για να την αγνοήσετε ή επανασυνδεθείτε για να αντιστοιχίσετε την απόδειξη.` + : `Δεν είναι δυνατή η αυτόματη αντιστοίχιση της απόδειξης, επειδή η σύνδεση με την τράπεζα χρειάζεται εκ νέου ταυτοποίηση. Επανασυνδεθείτε για να αντιστοιχίσετε την απόδειξη.`; + } + return isAdmin + ? `Η σύνδεση με την τράπεζα χρειάζεται εκ νέου ταυτοποίηση. Επανασυνδέστε για να αντιστοιχίσετε την απόδειξη` + : 'Η σύνδεση με την τράπεζα χρειάζεται εκ νέου ταυτοποίηση. Ζητήστε από ένα διαχειριστή να την επανασυνδέσει για να γίνει αντιστοίχιση της απόδειξης.'; + } if (isPersonalCard && (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || brokenBankConnection)) { if (!connectionLink) { return 'Δεν είναι δυνατή η αυτόματη αντιστοίχιση της απόδειξης λόγω σπασμένης σύνδεσης με την τράπεζα.'; @@ -10056,6 +10069,10 @@ ${reportName}`, adminBrokenConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => `Η απόδειξη εκκρεμεί λόγω κατεστραμμένης σύνδεσης τράπεζας. Παρακαλώ επιλύστε το στις εταιρικές κάρτες.`, memberBrokenConnectionError: 'Η απόδειξη εκκρεμεί λόγω προβλήματος στη σύνδεση με την τράπεζα. Παρακαλείστε να ζητήσετε από έναν διαχειριστή χώρου εργασίας να το επιλύσει.', + adminReauthConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => + `Η απόδειξη εκκρεμεί επειδή η σύνδεση με την τράπεζα χρειάζεται εκ νέου ταυτοποίηση. Παρακαλώ επιλύστε το στις εταιρικές κάρτες.`, + memberReauthConnectionError: + 'Η απόδειξη εκκρεμεί επειδή η σύνδεση με την τράπεζα χρειάζεται εκ νέου ταυτοποίηση. Παρακαλείστε να ζητήσετε από έναν διαχειριστή χώρου εργασίας να το επιλύσει.', markAsCashToIgnore: 'Σημειώστε ως μετρητά για να την αγνοήσετε και να ζητήσετε πληρωμή.', smartscanFailed: ({canEdit = true, missingFields = []}: {canEdit?: boolean; missingFields?: string[]}) => { if (missingFields.length > 0) { diff --git a/src/languages/en.ts b/src/languages/en.ts index 2f236444964a..4d8110181bf9 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9942,6 +9942,19 @@ const translations = { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return "Can't auto-match receipt due to broken bank connection."; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH) { + if (isPersonalCard) { + if (!connectionLink) { + return "Can't auto-match receipt because your bank connection needs re-authentication."; + } + return isMarkAsCash + ? `Can't auto-match receipt because your bank connection needs re-authentication. Mark as cash to ignore, or reconnect to match the receipt.` + : `Can't auto-match receipt because your bank connection needs re-authentication. Reconnect to match the receipt.`; + } + return isAdmin + ? `Bank connection needs re-authentication. Reconnect to match receipt` + : 'Bank connection needs re-authentication. Ask an admin to reconnect to match receipt.'; + } if (isPersonalCard && (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || brokenBankConnection)) { if (!connectionLink) { return "Can't auto-match receipt due to broken bank connection."; @@ -9965,6 +9978,9 @@ const translations = { adminBrokenConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => `Receipt pending due to broken bank connection. Please resolve in Company cards.`, memberBrokenConnectionError: 'Receipt pending due to broken bank connection. Please ask a workspace admin to resolve.', + adminReauthConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => + `Receipt pending because your bank connection needs re-authentication. Please resolve in Company cards.`, + memberReauthConnectionError: 'Receipt pending because your bank connection needs re-authentication. Please ask a workspace admin to resolve.', markAsCashToIgnore: 'Mark as cash to ignore and request payment.', smartscanFailed: ({canEdit = true, missingFields = []}: {canEdit?: boolean; missingFields?: string[]}) => { if (missingFields.length > 0) { diff --git a/src/languages/es.ts b/src/languages/es.ts index dccbd68d1f27..68f55850eba7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -10004,6 +10004,19 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'No se puede emparejar automáticamente el recibo debido a una conexión bancaria interrumpida.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH) { + if (isPersonalCard) { + if (!connectionLink) { + return 'No se puede emparejar automáticamente el recibo porque tu conexión bancaria necesita volver a autenticarse.'; + } + return isMarkAsCash + ? `No se puede emparejar automáticamente el recibo porque tu conexión bancaria necesita volver a autenticarse. Márcalo como efectivo para ignorarlo o vuelve a conectarte para asociar el recibo.` + : `No se puede emparejar automáticamente el recibo porque tu conexión bancaria necesita volver a autenticarse. Vuelve a conectarte para asociar el recibo.`; + } + return isAdmin + ? `La conexión bancaria necesita volver a autenticarse. Vuelve a conectarte para emparejar el recibo` + : 'La conexión bancaria necesita volver a autenticarse. Pide a un administrador que la vuelva a conectar para emparejar el recibo.'; + } if (isPersonalCard && (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || brokenBankConnection)) { if (!connectionLink) { return 'No se puede emparejar automáticamente el recibo debido a una conexión bancaria interrumpida.'; @@ -10028,6 +10041,10 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`, adminBrokenConnectionError: ({workspaceCompanyCardRoute}) => `Recibo pendiente debido a una conexión bancaria rota. Por favor, resuélvelo en Tarjetas de empresa.`, memberBrokenConnectionError: 'Recibo pendiente debido a una conexión bancaria rota. Por favor, pide a un administrador del espacio de trabajo que lo resuelva.', + adminReauthConnectionError: ({workspaceCompanyCardRoute}) => + `Recibo pendiente porque tu conexión bancaria necesita volver a autenticarse. Por favor, resuélvelo en Tarjetas de empresa.`, + memberReauthConnectionError: + 'Recibo pendiente porque tu conexión bancaria necesita volver a autenticarse. Por favor, pide a un administrador del espacio de trabajo que lo resuelva.', markAsCashToIgnore: 'Márcalo como efectivo para ignorar y solicitar el pago.', smartscanFailed: ({canEdit = true, missingFields = []}: {canEdit?: boolean; missingFields?: string[]}) => { if (missingFields.length > 0) { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 09a9ff5a7be5..83b35a7f5fa1 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -9839,6 +9839,19 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Impossible d’apparier automatiquement le reçu en raison d’une connexion bancaire rompue.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH) { + if (isPersonalCard) { + if (!connectionLink) { + return 'Impossible d’apparier automatiquement le reçu car votre connexion bancaire doit être ré-authentifiée.'; + } + return isMarkAsCash + ? `Impossible d’apparier automatiquement le reçu car votre connexion bancaire doit être ré-authentifiée. Marquez-le comme paiement en espèces pour l’ignorer, ou reconnectez-vous pour associer le reçu.` + : `Impossible d’apparier automatiquement le reçu car votre connexion bancaire doit être ré-authentifiée. Reconnectez-vous pour associer le reçu.`; + } + return isAdmin + ? `La connexion bancaire doit être ré-authentifiée. Reconnectez-vous pour faire correspondre le reçu` + : 'La connexion bancaire doit être ré-authentifiée. Demandez à un administrateur de la reconnecter pour faire correspondre le reçu.'; + } if (isPersonalCard && (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || brokenBankConnection)) { if (!connectionLink) { return 'Impossible d’apparier automatiquement le reçu en raison d’une connexion bancaire rompue.'; @@ -9861,6 +9874,10 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e adminBrokenConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => `Reçu en attente en raison d’une connexion bancaire rompue. Veuillez résoudre le problème dans les Cartes d’entreprise.`, memberBrokenConnectionError: 'Reçu en attente en raison d’une connexion bancaire rompue. Veuillez demander à un administrateur de l’espace de travail de résoudre le problème.', + adminReauthConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => + `Reçu en attente car la connexion bancaire doit être ré-authentifiée. Veuillez résoudre le problème dans les Cartes d’entreprise.`, + memberReauthConnectionError: + 'Reçu en attente car la connexion bancaire doit être ré-authentifiée. Veuillez demander à un administrateur de l’espace de travail de résoudre le problème.', markAsCashToIgnore: 'Marquer comme paiement en espèces pour l’ignorer et demander un paiement.', smartscanFailed: ({canEdit = true, missingFields = []}: {canEdit?: boolean; missingFields?: string[]}) => { if (missingFields.length > 0) { diff --git a/src/languages/it.ts b/src/languages/it.ts index c4b872f2159e..231c27eb946f 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -9776,6 +9776,19 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Impossibile abbinare automaticamente la ricevuta a causa di un problema di connessione bancaria.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH) { + if (isPersonalCard) { + if (!connectionLink) { + return 'Impossibile abbinare automaticamente la ricevuta perché la connessione bancaria deve essere ri-autenticata.'; + } + return isMarkAsCash + ? `Impossibile abbinare automaticamente la ricevuta perché la connessione bancaria deve essere ri-autenticata. Contrassegna come contanti per ignorare oppure riconnettiti per abbinare la ricevuta.` + : `Impossibile abbinare automaticamente la ricevuta perché la connessione bancaria deve essere ri-autenticata. Riconnettiti per abbinare la ricevuta.`; + } + return isAdmin + ? `La connessione bancaria deve essere ri-autenticata. Riconnetti per abbinare la ricevuta` + : 'La connessione bancaria deve essere ri-autenticata. Chiedi a un amministratore di riconnetterla per abbinare la ricevuta.'; + } if (isPersonalCard && (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || brokenBankConnection)) { if (!connectionLink) { return 'Impossibile abbinare automaticamente la ricevuta a causa di un problema di connessione bancaria.'; @@ -9798,6 +9811,10 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, adminBrokenConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => `Ricevuta in sospeso a causa di un collegamento bancario interrotto. Risolvi il problema in Carte aziendali.`, memberBrokenConnectionError: 'Ricevuta in sospeso a causa di un collegamento bancario interrotto. Chiedi a un amministratore dello spazio di lavoro di risolvere il problema.', + adminReauthConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => + `Ricevuta in sospeso perché la connessione bancaria deve essere ri-autenticata. Risolvi il problema in Carte aziendali.`, + memberReauthConnectionError: + 'Ricevuta in sospeso perché la connessione bancaria deve essere ri-autenticata. Chiedi a un amministratore dello spazio di lavoro di risolvere il problema.', markAsCashToIgnore: 'Segna come contante da ignorare e richiedi il pagamento.', smartscanFailed: ({canEdit = true, missingFields = []}: {canEdit?: boolean; missingFields?: string[]}) => { if (missingFields.length > 0) { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index adaccf059f0b..dcd9d40d7b33 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -9647,6 +9647,19 @@ ${reportName}`, if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return '銀行連携の不具合により、領収書を自動照合できません。'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH) { + if (isPersonalCard) { + if (!connectionLink) { + return '銀行連携の再認証が必要なため、領収書を自動照合できません。'; + } + return isMarkAsCash + ? `銀行連携の再認証が必要なため、領収書を自動照合できません。無視するには現金としてマークするか、再接続して領収書と照合してください。` + : `銀行連携の再認証が必要なため、領収書を自動照合できません。領収書を照合するには、再接続してください。`; + } + return isAdmin + ? `銀行連携の再認証が必要です。レシートと照合するために再接続` + : '銀行連携の再認証が必要です。管理者に依頼して再接続し、領収書と照合してください。'; + } if (isPersonalCard && (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || brokenBankConnection)) { if (!connectionLink) { return '銀行連携の不具合により、領収書を自動照合できません。'; @@ -9669,6 +9682,9 @@ ${reportName}`, adminBrokenConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => `銀行接続の不具合により領収書が保留されています。会社カードで解決してください。`, memberBrokenConnectionError: '銀行連携の不具合により領収書が保留されています。ワークスペース管理者に対応を依頼してください。', + adminReauthConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => + `銀行連携の再認証が必要なため、領収書が保留されています。会社カードで解決してください。`, + memberReauthConnectionError: '銀行連携の再認証が必要なため、領収書が保留されています。ワークスペース管理者に対応を依頼してください。', markAsCashToIgnore: '現金としてマークして無視し、支払いをリクエストします。', smartscanFailed: ({canEdit = true, missingFields = []}: {canEdit?: boolean; missingFields?: string[]}) => { if (missingFields.length > 0) { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 40398c2fe6c8..051a7e19374f 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -9746,6 +9746,19 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Bon kan automatisch aan bon koppelen vanwege verbroken bankverbinding.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH) { + if (isPersonalCard) { + if (!connectionLink) { + return 'Kan bon niet automatisch koppelen omdat je bankverbinding opnieuw geverifieerd moet worden.'; + } + return isMarkAsCash + ? `Kan bon niet automatisch koppelen omdat je bankverbinding opnieuw geverifieerd moet worden. Markeer als contant om te negeren, of verbind opnieuw om de bon te koppelen.` + : `Kan bon niet automatisch koppelen omdat je bankverbinding opnieuw geverifieerd moet worden. Verbind opnieuw om de bon te koppelen.`; + } + return isAdmin + ? `Bankkoppeling moet opnieuw worden geverifieerd. Opnieuw verbinden om bon te koppelen` + : 'Bankkoppeling moet opnieuw worden geverifieerd. Vraag een beheerder om de verbinding opnieuw te maken om de bon te laten overeenkomen.'; + } if (isPersonalCard && (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || brokenBankConnection)) { if (!connectionLink) { return 'Bon kan automatisch aan bon koppelen vanwege verbroken bankverbinding.'; @@ -9768,6 +9781,9 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, adminBrokenConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => `Bon in behandeling vanwege een verbroken bankverbinding. Los dit op in Bedrijfspassen.`, memberBrokenConnectionError: 'Bon wordt vastgehouden vanwege een verbroken bankkoppeling. Vraag een werkruimtebeheerder om dit op te lossen.', + adminReauthConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => + `Bon in behandeling omdat de bankverbinding opnieuw geverifieerd moet worden. Los dit op in Bedrijfspassen.`, + memberReauthConnectionError: 'Bon wordt vastgehouden omdat de bankkoppeling opnieuw geverifieerd moet worden. Vraag een werkruimtebeheerder om dit op te lossen.', markAsCashToIgnore: 'Markeren als contant om te negeren en betaling aan te vragen.', smartscanFailed: ({canEdit = true, missingFields = []}: {canEdit?: boolean; missingFields?: string[]}) => { if (missingFields.length > 0) { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 38a4dae9bad2..85aa02f580a6 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -9718,6 +9718,19 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Nie można automatycznie dopasować paragonu z powodu zerwanego połączenia z bankiem.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH) { + if (isPersonalCard) { + if (!connectionLink) { + return 'Nie można automatycznie dopasować paragonu, ponieważ połączenie z bankiem wymaga ponownego uwierzytelnienia.'; + } + return isMarkAsCash + ? `Nie można automatycznie dopasować paragonu, ponieważ połączenie z bankiem wymaga ponownego uwierzytelnienia. Oznacz jako gotówkę, aby zignorować, lub połącz ponownie, aby dopasować paragon.` + : `Nie można automatycznie dopasować paragonu, ponieważ połączenie z bankiem wymaga ponownego uwierzytelnienia. Połącz ponownie, aby dopasować paragon.`; + } + return isAdmin + ? `Połączenie z bankiem wymaga ponownego uwierzytelnienia. Połącz ponownie, aby dopasować paragon` + : 'Połączenie z bankiem wymaga ponownego uwierzytelnienia. Poproś administratora o ponowne połączenie, aby dopasować paragon.'; + } if (isPersonalCard && (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || brokenBankConnection)) { if (!connectionLink) { return 'Nie można automatycznie dopasować paragonu z powodu zerwanego połączenia z bankiem.'; @@ -9740,6 +9753,9 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, adminBrokenConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => `Oczekuje na rachunek z powodu przerwanego połączenia z bankiem. Rozwiąż problem w sekcji Karty firmowe.`, memberBrokenConnectionError: 'Paragon oczekuje z powodu zerwanego połączenia z bankiem. Poproś administratora przestrzeni roboczej o rozwiązanie problemu.', + adminReauthConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => + `Paragon oczekuje, ponieważ połączenie z bankiem wymaga ponownego uwierzytelnienia. Rozwiąż problem w sekcji Karty firmowe.`, + memberReauthConnectionError: 'Paragon oczekuje, ponieważ połączenie z bankiem wymaga ponownego uwierzytelnienia. Poproś administratora przestrzeni roboczej o rozwiązanie problemu.', markAsCashToIgnore: 'Oznacz jako gotówkę, aby zignorować i poprosić o płatność.', smartscanFailed: ({canEdit = true, missingFields = []}: {canEdit?: boolean; missingFields?: string[]}) => { if (missingFields.length > 0) { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index dcad147ca7d9..128d449951cf 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -9741,6 +9741,19 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Não é possível conciliar automaticamente o recibo devido a uma conexão bancária com erro.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH) { + if (isPersonalCard) { + if (!connectionLink) { + return 'Não é possível conciliar automaticamente o recibo porque sua conexão bancária precisa ser reautenticada.'; + } + return isMarkAsCash + ? `Não é possível conciliar automaticamente o recibo porque sua conexão bancária precisa ser reautenticada. Marque como dinheiro para ignorar ou reconecte para corresponder o recibo.` + : `Não é possível conciliar automaticamente o recibo porque sua conexão bancária precisa ser reautenticada. Reconecte para corresponder o recibo.`; + } + return isAdmin + ? `A conexão bancária precisa ser reautenticada. Reconectar para corresponder ao recibo` + : 'A conexão bancária precisa ser reautenticada. Peça para um administrador reconectar para corresponder ao recibo.'; + } if (isPersonalCard && (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || brokenBankConnection)) { if (!connectionLink) { return 'Não é possível conciliar automaticamente o recibo devido a uma conexão bancária com erro.'; @@ -9763,6 +9776,9 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, adminBrokenConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => `Recibo pendente devido a uma conexão bancária interrompida. Resolva em Cartões corporativos.`, memberBrokenConnectionError: 'Recibo pendente devido à conexão bancária com problemas. Peça a um administrador do workspace para resolver.', + adminReauthConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => + `Recibo pendente porque a conexão bancária precisa ser reautenticada. Resolva em Cartões corporativos.`, + memberReauthConnectionError: 'Recibo pendente porque a conexão bancária precisa ser reautenticada. Peça a um administrador do workspace para resolver.', markAsCashToIgnore: 'Marcar como dinheiro para ignorar e solicitar pagamento.', smartscanFailed: ({canEdit = true, missingFields = []}: {canEdit?: boolean; missingFields?: string[]}) => { if (missingFields.length > 0) { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index d53d3e5603d2..bc18eca2767d 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -9401,6 +9401,17 @@ ${reportName}`, if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return '由于银行连接中断,无法自动匹配收据。'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH) { + if (isPersonalCard) { + if (!connectionLink) { + return '由于您的银行连接需要重新验证,无法自动匹配收据。'; + } + return isMarkAsCash + ? `由于您的银行连接需要重新验证,无法自动匹配收据。标记为现金以忽略,或重新连接以匹配收据。` + : `由于您的银行连接需要重新验证,无法自动匹配收据。请重新连接以匹配该收据。`; + } + return isAdmin ? `银行连接需要重新验证。重新连接以匹配收据` : '银行连接需要重新验证。请让管理员重新连接以匹配收据。'; + } if (isPersonalCard && (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || brokenBankConnection)) { if (!connectionLink) { return '由于银行连接中断,无法自动匹配收据。'; @@ -9421,6 +9432,9 @@ ${reportName}`, adminBrokenConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => `由于银行连接中断,收据暂时待处理。请前往公司卡中解决。`, memberBrokenConnectionError: '由于银行连接中断,收据处于待处理状态。请联系工作区管理员解决。', + adminReauthConnectionError: ({workspaceCompanyCardRoute}: {workspaceCompanyCardRoute: string}) => + `由于银行连接需要重新验证,收据暂时待处理。请前往公司卡中解决。`, + memberReauthConnectionError: '由于银行连接需要重新验证,收据处于待处理状态。请联系工作区管理员解决。', markAsCashToIgnore: '标记为现金以忽略并请求付款。', smartscanFailed: ({canEdit = true, missingFields = []}: {canEdit?: boolean; missingFields?: string[]}) => { if (missingFields.length > 0) { diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 734d3253be78..1fea7b94ccdc 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1832,7 +1832,8 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | transactionViolation.name === CONST.VIOLATIONS.RTER && transactionViolation.data?.pendingPattern && transactionViolation.data?.rterType !== CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION && - transactionViolation.data?.rterType !== CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530, + transactionViolation.data?.rterType !== CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530 && + transactionViolation.data?.rterType !== CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH, ); } @@ -1880,7 +1881,9 @@ function hasBrokenConnectionViolation( function isBrokenConnectionViolation(violation: TransactionViolation) { return ( violation.name === CONST.VIOLATIONS.RTER && - (violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) + (violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || + violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530 || + violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH) ); } diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index 1a8045cc822a..647c86785db6 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -1293,6 +1293,14 @@ describe('TransactionUtils', () => { expect(showBrokenConnectionViolation).toBe(true); }); + it('should return true when a re-auth broken connection violation exists and the user is the policy member', () => { + const policy = createMock({role: CONST.POLICY.ROLE.USER}); + const transactionViolations = [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH}}]; + const showBrokenConnectionViolation = shouldShowBrokenConnectionViolation(undefined, policy, transactionViolations); + + expect(showBrokenConnectionViolation).toBe(true); + }); + it('should return true when a broken connection violation exists for any of the provided transactions and the user is the policy member', () => { const policy = createMock({ role: CONST.POLICY.ROLE.USER, @@ -1360,6 +1368,24 @@ describe('TransactionUtils', () => { }); }); + describe('hasPendingRTERViolation', () => { + it('should not treat a re-auth broken connection violation as pending', () => { + const transactionViolations = [ + {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {pendingPattern: true, rterType: CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH}}, + ]; + + expect(TransactionUtils.hasPendingRTERViolation(transactionViolations)).toBe(false); + }); + + it('should treat a seven-day-hold RTER violation as pending', () => { + const transactionViolations = [ + {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.RTER, data: {pendingPattern: true, rterType: CONST.RTER_VIOLATION_TYPES.SEVEN_DAY_HOLD}}, + ]; + + expect(TransactionUtils.hasPendingRTERViolation(transactionViolations)).toBe(true); + }); + }); + describe('isMerchantMissing', () => { it('returns true for empty, default, and partial merchant values', () => { expect(TransactionUtils.isMerchantMissing(generateTransaction({merchant: ''}))).toBe(true); diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index 1e7cf440f632..63960ebaa5cd 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -3443,6 +3443,33 @@ describe('getViolationTranslation', () => { ); }); + it('should return the re-authentication message for the re-auth rterType (438/532)', () => { + const testPolicyID = 'test-policy-123'; + const companyCardPageURL = `workspaces/${testPolicyID}/company-cards`; + const connectionLink = `settings/wallet/card/${testPolicyID}`; + const reauthType = CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH; + + // Company card, admin + expect(translateLocal('violations.rter', false, true, false, undefined, reauthType, companyCardPageURL, undefined, false, false)).toBe( + `Bank connection needs re-authentication. Reconnect to match receipt`, + ); + + // Company card, non-admin member + expect(translateLocal('violations.rter', false, false, false, undefined, reauthType, companyCardPageURL, undefined, false, false)).toBe( + 'Bank connection needs re-authentication. Ask an admin to reconnect to match receipt.', + ); + + // Personal card, not mark-as-cash + expect(translateLocal('violations.rter', false, false, false, undefined, reauthType, companyCardPageURL, connectionLink, true, false)).toBe( + `Can't auto-match receipt because your bank connection needs re-authentication. Reconnect to match the receipt.`, + ); + + // Personal card, mark-as-cash + expect(translateLocal('violations.rter', false, false, false, undefined, reauthType, companyCardPageURL, connectionLink, true, true)).toBe( + `Can't auto-match receipt because your bank connection needs re-authentication. Mark as cash to ignore, or reconnect to match the receipt.`, + ); + }); + describe('increasedDistance violation', () => { const increasedDistanceViolation: TransactionViolation = { name: CONST.VIOLATIONS.INCREASED_DISTANCE, From 0f82451eb674c0b22594e5221e4db44f3720ed5d Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 14 Aug 2026 09:29:37 -0600 Subject: [PATCH 2/2] Include re-auth type in report header personal-card suppression guard --- src/hooks/useMoneyReportHeaderStatusBar.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/hooks/useMoneyReportHeaderStatusBar.ts b/src/hooks/useMoneyReportHeaderStatusBar.ts index 2f45d7a3d185..66a2639f1ea0 100644 --- a/src/hooks/useMoneyReportHeaderStatusBar.ts +++ b/src/hooks/useMoneyReportHeaderStatusBar.ts @@ -122,7 +122,10 @@ function useMoneyReportHeaderStatusBar(reportID: string | undefined, chatReportI return CONST.REPORT.STATUS_BAR_TYPE.DUPLICATES; } if (!!transaction?.transactionID && !!transactionViolations.length && shouldShowBrokenConnectionViolation) { - const brokenConnectionError = transactionViolations.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION); + const brokenConnectionError = transactionViolations.find( + (violation) => + violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_REAUTH, + ); const cardID = brokenConnectionError?.data?.cardID; const card = cardID ? cardList?.[cardID] : undefined; if (isPersonalCard(card) && brokenConnectionError) {