From c5cab64c16c6ba716b70f6ddd7b3b314767925a4 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Tue, 13 Jan 2026 09:49:53 +0200 Subject: [PATCH 1/7] Personal card broken connection --- src/CONST/index.ts | 2 ++ src/languages/en.ts | 14 ++++++++++++- src/languages/params.ts | 7 +++++++ src/libs/CardUtils.ts | 10 ++++++++++ src/libs/ReportActionsUtils.ts | 20 +++++++++++++++++-- .../home/report/PureReportActionItem.tsx | 12 +++++++++++ src/pages/home/report/ReportActionItem.tsx | 3 +++ src/types/onyx/OriginalMessage.ts | 12 +++++++++++ 8 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 69256dd64a7c0..b0d2c1fc118d0 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1227,6 +1227,7 @@ const CONST = { CARD_REPLACED_VIRTUAL: 'CARDREPLACEDVIRTUAL', CARD_REPLACED: 'CARDREPLACED', CARD_ASSIGNED: 'CARDASSIGNED', + CARD_CONNECTION_BROKEN: 'CARDCONNECTIONBROKEN', CHANGE_FIELD: 'CHANGEFIELD', // OldDot Action CHANGE_POLICY: 'CHANGEPOLICY', CREATED_REPORT_FOR_UNAPPROVED_TRANSACTIONS: 'CREATEDREPORTFORUNAPPROVEDTRANSACTIONS', @@ -5783,6 +5784,7 @@ const CONST = { }, RTER_VIOLATION_TYPES: { BROKEN_CARD_CONNECTION: 'brokenCardConnection', + BROKEN_PERSONAL_CARD_CONNECTION: 'brokenPersonalCardConnection', BROKEN_CARD_CONNECTION_530: 'brokenCardConnection530', SEVEN_DAY_HOLD: 'sevenDayHold', }, diff --git a/src/languages/en.ts b/src/languages/en.ts index 0e229cbbdebd0..03b6d88a22383 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -8,6 +8,7 @@ import type {Country} from '@src/CONST'; import type OriginalMessage from '@src/types/onyx/OriginalMessage'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2061,6 +2062,11 @@ const translations = { password: 'Please enter your Expensify password', }, }, + personalCard: { + brokenConnection: 'Your card connection is broken', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `Your ${cardName} card connection is broken. Log into your bank to fix the card.`, + }, walletPage: { balance: 'Balance', paymentMethodsTitle: 'Payment methods', @@ -7209,7 +7215,7 @@ const translations = { }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Review required', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return "Can't auto-match receipt due to broken bank connection"; } @@ -7222,6 +7228,12 @@ const translations = { return isAdmin ? `Ask ${member} to mark as a cash or wait 7 days and try again` : 'Awaiting merge with card transaction.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `Can’t auto-match receipt due to broken card connection. Mark as cash to ignore, or fix the card to match the receipt.` + : "Can't auto-match receipt due to broken card connection."; + } + return ''; }, brokenConnection530Error: 'Receipt pending due to broken bank connection', diff --git a/src/languages/params.ts b/src/languages/params.ts index a188c3263caa6..e341b39a38627 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -234,6 +234,7 @@ type ViolationsRterParams = { member?: string; rterType?: ValueOf; companyCardPageURL?: string; + connectionLink?: string; }; type ViolationsTagOutOfPolicyParams = {tagName?: string} | undefined; @@ -699,6 +700,11 @@ type RoutedDueToDEWParams = { to: string; }; +type ConciergeBrokenCardConnectionParams = { + cardName: string; + connectionLink: string; +}; + export type { SettlementAccountReconciliationParams, ToggleImportTitleParams, @@ -777,6 +783,7 @@ export type { RemovedTheRequestParams, MovedFromReportParams, RenamedRoomActionParams, + ConciergeBrokenCardConnectionParams, ReportArchiveReasonsClosedParams, ReportArchiveReasonsMergedParams, ReportPolicyNameParams, diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 6bbefbe67f06b..4ffd16d1b8e48 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -747,6 +747,15 @@ function isCardPendingReplace(card?: Card) { ); } +/** + * Check if card has a broken connection + * + * @param card personal card to check + */ +function isPersonalCardBrokenConnection(card?: Card) { + return card?.lastScrapeResult && !CONST.COMPANY_CARDS.BROKEN_CONNECTION_IGNORED_STATUSES.includes(card?.lastScrapeResult); +} + function isExpensifyCardPendingAction(card?: Card, privatePersonalDetails?: PrivatePersonalDetails): boolean { return ( card?.bank === CONST.EXPENSIFY_CARD.BANK && @@ -921,6 +930,7 @@ export { getBankName, isSelectedFeedExpired, getCompanyFeeds, + isPersonalCardBrokenConnection, isCustomFeed, getBankCardDetailsImage, getSelectedFeed, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8c39e5eacc946..fba64bc143672 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -13,7 +13,7 @@ import IntlStore from '@src/languages/IntlStore'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Card, OnyxInputOrEntry, OriginalMessageIOU, PersonalDetails, Policy, PrivatePersonalDetails, ReportMetadata, ReportNameValuePairs} from '@src/types/onyx'; +import type {Card, CompanyCardFeed, OnyxInputOrEntry, OriginalMessageIOU, PersonalDetails, Policy, PrivatePersonalDetails, ReportMetadata, ReportNameValuePairs} from '@src/types/onyx'; import type { JoinWorkspaceResolution, OriginalMessageChangeLog, @@ -27,7 +27,7 @@ import type ReportAction from '@src/types/onyx/ReportAction'; import type {Message, OldDotReportAction, OriginalMessage, ReportActions} from '@src/types/onyx/ReportAction'; import type ReportActionName from '@src/types/onyx/ReportActionName'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {isCardPendingActivate} from './CardUtils'; +import {getBankName, getPlaidInstitutionId, isCardPendingActivate, isPersonalCardBrokenConnection} from './CardUtils'; import {getDecodedCategoryName} from './CategoryUtils'; import {convertAmountToDisplayString, convertToDisplayString, convertToShortDisplayString} from './CurrencyUtils'; import DateUtils from './DateUtils'; @@ -362,6 +362,10 @@ function isHoldAction(reportAction: OnyxInputOrEntry): reportActio return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.HOLD); } +function isCardBrokenConnectionAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction { + return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.CARD_CONNECTION_BROKEN); +} + function isReimbursementDirectionInformationRequiredAction( reportAction: OnyxInputOrEntry, ): reportAction is ReportAction { @@ -3496,6 +3500,16 @@ function getCardIssuedMessage({ } } +function getCardConnectionBrokenMessage(reportAction: OnyxEntry, card: Card | undefined, translate: LocaleContextProps['translate']) { + if (!isCardBrokenConnectionAction(reportAction) || !isPersonalCardBrokenConnection(card)) { + return ''; + } + const cardName = card?.cardName; + const isPlaid = !!getPlaidInstitutionId(card?.bank); + const personalCardName = isPlaid && cardName ? cardName : getBankName(card?.bank as CompanyCardFeed); + return translate('personalCard.conciergeBrokenConnection', {cardName: personalCardName, connectionLink: ''}); +} + function getRoomChangeLogMessage(translate: LocalizedTranslate, reportAction: ReportAction) { if (!isInviteOrRemovedAction(reportAction)) { return ''; @@ -3714,6 +3728,8 @@ export { isHoldAction, isWhisperAction, isSubmittedAction, + isCardBrokenConnectionAction, + getCardConnectionBrokenMessage, isSubmittedAndClosedAction, isDynamicExternalWorkflowSubmitAction, isMarkAsClosedAction, diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 41178c62cc31b..c4ea2b551c146 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -68,6 +68,7 @@ import { getActionableMentionWhisperMessage, getAddedApprovalRuleMessage, getAddedConnectionMessage, + getCardConnectionBrokenMessage, getChangedApproverActionMessage, getCompanyAddressUpdateMessage, getCompanyCardConnectionBrokenMessage, @@ -129,6 +130,7 @@ import { isActionableReportMentionWhisper, isActionableTrackExpense, isActionOfType, + isCardBrokenConnectionAction, isCardIssuedAction, isChronosOOOListAction, isConciergeCategoryOptions, @@ -207,6 +209,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; +import type {CardList} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {JoinWorkspaceResolution, OriginalMessageMovedTransaction, OriginalMessageUnreportedTransaction} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject, isEmptyValueObject} from '@src/types/utils/EmptyObject'; @@ -341,6 +344,9 @@ type PureReportActionItemProps = { /** Whether the room is a chronos report */ isChronosReport?: boolean; + /** All cards */ + cardList?: CardList; + /** Function to toggle emoji reaction */ toggleEmojiReaction?: ( reportID: string | undefined, @@ -472,6 +478,7 @@ function PureReportActionItem({ iouReportOfLinkedReport, emojiReactions, linkedTransactionRouteError, + cardList, isUserValidated, parentReport, personalDetails, @@ -1524,6 +1531,10 @@ function PureReportActionItem({ policyID={report?.policyID} /> ); + } else if (isCardBrokenConnectionAction(action)) { + const cardID = getOriginalMessage(action)?.cardID; + const card = cardID ? cardList?.[cardID] : undefined; + children = ; } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION)) { children = ; } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.RECEIPT_SCAN_FAILED)) { @@ -2023,6 +2034,7 @@ export default memo(PureReportActionItem, (prevProps, nextProps) => { deepEqual(prevProps.taskReport, nextProps.taskReport) && prevProps.shouldHighlight === nextProps.shouldHighlight && deepEqual(prevProps.bankAccountList, nextProps.bankAccountList) && + deepEqual(prevProps.cardList, nextProps.cardList) && prevProps.reportNameValuePairsOrigin === nextProps.reportNameValuePairsOrigin && prevProps.reportNameValuePairsOriginalID === nextProps.reportNameValuePairsOriginalID && prevProps.reportMetadata?.pendingExpenseAction === nextProps.reportMetadata?.pendingExpenseAction diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index ab6833febcc0d..393562b17720a 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -6,6 +6,7 @@ import useOnyx from '@hooks/useOnyx'; import useOriginalReportID from '@hooks/useOriginalReportID'; import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; import useReportIsArchived from '@hooks/useReportIsArchived'; +import {filterPersonalCards} from '@libs/CardUtils'; import {getForReportAction, getMovedReportID} from '@libs/ModifiedExpenseMessage'; import {getIOUReportIDFromReportActionPreview, getOriginalMessage} from '@libs/ReportActionsUtils'; import { @@ -101,6 +102,7 @@ function ReportActionItem({ const movedFromReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(action, CONST.REPORT.MOVE_TYPE.FROM)}`]; const movedToReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(action, CONST.REPORT.MOVE_TYPE.TO)}`]; const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: false}); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); // The app would crash due to subscribing to the entire report collection if parentReportID is an empty string. So we should have a fallback ID here. @@ -128,6 +130,7 @@ function ReportActionItem({ draftMessage={draftMessage} iouReport={iouReport} taskReport={taskReport} + cardList={cardList} linkedReport={linkedReport} iouReportOfLinkedReport={iouReportOfLinkedReport} emojiReactions={emojiReactions} diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 661fde10f1cc2..cacf5bf0d4567 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -1043,6 +1043,17 @@ type OriginalMessageCard = { hadMissingAddress?: boolean; }; +/** + * Model of CARD_CONNECTION_BROKEN action + */ +type OriginalPersonalCard = { + /** The id of the user the card was assigned to */ + assigneeAccountID: number; + + /** The id of the card */ + cardID: number; +}; + /** * Model of INTEGRATIONS_MESSAGE report action */ @@ -1180,6 +1191,7 @@ type OriginalMessageMap = { [CONST.REPORT.ACTIONS.TYPE.CARD_REPLACED_VIRTUAL]: OriginalMessageCard; [CONST.REPORT.ACTIONS.TYPE.CARD_REPLACED]: OriginalMessageCard; [CONST.REPORT.ACTIONS.TYPE.CARD_ASSIGNED]: OriginalMessageCard; + [CONST.REPORT.ACTIONS.TYPE.CARD_CONNECTION_BROKEN]: OriginalPersonalCard; [CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED]: OriginalMessageIntegrationSyncFailed; [CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION]: OriginalMessageDeletedTransaction; [CONST.REPORT.ACTIONS.TYPE.DEW_SUBMIT_FAILED]: OriginalMessageDEWFailed; From 86b0635018003e70462d636414d9bafdadad3a35 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Tue, 13 Jan 2026 17:08:00 +0200 Subject: [PATCH 2/7] add translations and violation logic --- .../ReportActionItem/MoneyRequestReceiptView.tsx | 6 ++++-- .../ReportActionItem/MoneyRequestView.tsx | 8 +++++--- .../TransactionPreviewContent.tsx | 4 +++- .../TransactionItemRow/TransactionItemRowRBR.tsx | 3 +++ src/components/ViolationMessages.tsx | 5 +++-- src/languages/de.ts | 13 ++++++++++++- src/languages/es.ts | 13 ++++++++++++- src/languages/fr.ts | 13 ++++++++++++- src/languages/it.ts | 13 ++++++++++++- src/languages/ja.ts | 13 ++++++++++++- src/languages/nl.ts | 13 ++++++++++++- src/languages/pl.ts | 13 ++++++++++++- src/languages/pt-BR.ts | 13 ++++++++++++- src/languages/zh-hans.ts | 13 ++++++++++++- src/libs/Violations/ViolationsUtils.ts | 13 +++++++++++-- 15 files changed, 137 insertions(+), 19 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index f8d4347c71ac9..f0d9378e2e360 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -135,6 +135,8 @@ function MoneyRequestReceiptView({ const isEditable = !!canUserPerformWriteActionReportUtils(report, isReportArchived) && !readonly; const canEdit = isMoneyRequestAction(parentReportAction) && canEditMoneyRequest(parentReportAction, isChatReportArchived, moneyRequestReport, policy, transaction) && isEditable; const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(report?.policyID)}`; + // TODO add correct link to card page in wallet settings + const connectionLink = `${environmentURL}/${ROUTES.SETTINGS_WALLET}`; const canEditReceipt = isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT, undefined, isChatReportArchived); @@ -169,7 +171,7 @@ function MoneyRequestReceiptView({ const isReceiptFieldViolation = receiptFieldViolationNames.has(violation.name); const isReceiptImageViolation = receiptImageViolationNames.has(violation.name); if (isReceiptFieldViolation || isReceiptImageViolation) { - const violationMessage = ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL); + const violationMessage = ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL, connectionLink); allViolations.push(violationMessage); if (isReceiptImageViolation) { imageViolations.push(violationMessage); @@ -177,7 +179,7 @@ function MoneyRequestReceiptView({ } } return [imageViolations, allViolations]; - }, [transactionViolations, translate, canEdit, companyCardPageURL]); + }, [transactionViolations, translate, canEdit, companyCardPageURL, connectionLink]); const receiptRequiredViolation = transactionViolations?.some((violation) => violation.name === CONST.VIOLATIONS.RECEIPT_REQUIRED); const customRulesViolation = transactionViolations?.some((violation) => violation.name === CONST.VIOLATIONS.CUSTOM_RULES); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index d850e5941ef63..cc449364380d9 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -55,8 +55,7 @@ import {isSplitAction} from '@libs/ReportSecondaryActionUtils'; import { canEditFieldOfMoneyRequest, canEditMoneyRequest, - canUserPerformWriteAction as canUserPerformWriteActionReportUtils, - // eslint-disable-next-line @typescript-eslint/no-deprecated + canUserPerformWriteAction as canUserPerformWriteActionReportUtils, // eslint-disable-next-line @typescript-eslint/no-deprecated getReportName, getTransactionDetails, getTripIDFromTransactionParentReportID, @@ -320,6 +319,8 @@ function MoneyRequestView({ const isEditable = !!canUserPerformWriteActionReportUtils(transactionThreadReport, isReportArchived) && !readonly; const canEdit = isMoneyRequestAction(parentReportAction) && canEditMoneyRequest(parentReportAction, isChatReportArchived, moneyRequestReport, policy, transaction) && isEditable; const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(transactionThreadReport?.policyID)}`; + // TODO add correct link to card page in wallet settings + const connectionLink = `${environmentURL}/${ROUTES.SETTINGS_WALLET}`; const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transaction?.comment?.originalTransactionID)}`, {canBeMissing: true}); const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction); const isSplitAvailable = moneyRequestReport && transaction && isSplitAction(moneyRequestReport, [transaction], originalTransaction, currentUserPersonalDetails.login ?? '', policy); @@ -518,7 +519,7 @@ function MoneyRequestView({ // Return violations if there are any if (field !== 'merchant' && hasViolations(field, data, policyHasDependentTags, tagValue)) { const violations = getViolationsForField(field, data, policyHasDependentTags, tagValue); - return `${violations.map((violation) => ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL)).join('. ')}.`; + return `${violations.map((violation) => ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL, connectionLink)).join('. ')}.`; } return ''; @@ -1073,6 +1074,7 @@ function MoneyRequestView({ isLast canEdit={canEdit} companyCardPageURL={companyCardPageURL} + connectionLink={connectionLink} /> )} diff --git a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx index a417b3917126d..623d41df0cf87 100644 --- a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx +++ b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx @@ -117,7 +117,9 @@ function TransactionPreviewContent({ const isIOUActionType = isMoneyRequestAction(action); const canEdit = isIOUActionType && canEditMoneyRequest(action, isChatReportArchived, report, policy, transaction); const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(report?.policyID)}`; - const violationMessage = firstViolation ? ViolationsUtils.getViolationTranslation(firstViolation, translate, canEdit, undefined, companyCardPageURL) : undefined; + // TODO add correct link to card page in wallet settings + const connectionLink = `${environmentURL}/${ROUTES.SETTINGS_WALLET}`; + const violationMessage = firstViolation ? ViolationsUtils.getViolationTranslation(firstViolation, translate, canEdit, undefined, companyCardPageURL, connectionLink) : undefined; const previewText = useMemo( () => diff --git a/src/components/TransactionItemRow/TransactionItemRowRBR.tsx b/src/components/TransactionItemRow/TransactionItemRowRBR.tsx index bbf0d73916d84..c0b8813543d30 100644 --- a/src/components/TransactionItemRow/TransactionItemRowRBR.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowRBR.tsx @@ -44,6 +44,8 @@ function TransactionItemRowRBR({transaction, violations, report, containerStyles canBeMissing: true, }); const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(report?.policyID)}`; + // TODO add correct link to card page in wallet settings + const connectionLink = `${environmentURL}/${ROUTES.SETTINGS_WALLET}`; const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: true}); const transactionThreadId = reportActions ? getIOUActionForTransactionID(Object.values(reportActions ?? {}), transaction.transactionID)?.childReportID : undefined; const [transactionThreadActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadId}`, { @@ -58,6 +60,7 @@ function TransactionItemRowRBR({transaction, violations, report, containerStyles Object.values(transactionThreadActions ?? {}), policyTags, companyCardPageURL, + connectionLink, ); return ( diff --git a/src/components/ViolationMessages.tsx b/src/components/ViolationMessages.tsx index 77b17dd44fad2..a386c6271381e 100644 --- a/src/components/ViolationMessages.tsx +++ b/src/components/ViolationMessages.tsx @@ -14,13 +14,14 @@ type ViolationMessagesProps = { textStyle?: StyleProp; canEdit: boolean; companyCardPageURL?: string; + connectionLink?: string; }; -export default function ViolationMessages({violations, isLast, containerStyle, textStyle, canEdit, companyCardPageURL}: ViolationMessagesProps) { +export default function ViolationMessages({violations, isLast, containerStyle, textStyle, canEdit, companyCardPageURL, connectionLink}: ViolationMessagesProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const violationMessages = useMemo( - () => violations.map((violation) => [violation.name, ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL)]), + () => violations.map((violation) => [violation.name, ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL, connectionLink)]), [canEdit, translate, violations, companyCardPageURL], ); diff --git a/src/languages/de.ts b/src/languages/de.ts index 02a8634dcd8a2..1b98ef993ed4f 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -20,6 +20,7 @@ import type OriginalMessage from '@src/types/onyx/OriginalMessage'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2073,6 +2074,11 @@ const translations: TranslationDeepObject = { password: 'Bitte geben Sie Ihr Expensify-Passwort ein', }, }, + personalCard: { + brokenConnection: 'Ihre Kartenverbindung ist unterbrochen.', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `Die Verbindung zu Ihrer Karte ${cardName} ist unterbrochen. Melden Sie sich bei Ihrem Online-Banking an, um die Karte zu reparieren.`, + }, walletPage: { balance: 'Kontostand', paymentMethodsTitle: 'Zahlungsmethoden', @@ -7279,7 +7285,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Überprüfung erforderlich', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Beleg kann aufgrund einer unterbrochenen Bankverbindung nicht automatisch zugeordnet werden'; } @@ -7291,6 +7297,11 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard if (!isTransactionOlderThan7Days) { return isAdmin ? `Bitte ${member} bitten, es als Barzahlung zu markieren, oder warte 7 Tage und versuche es erneut` : 'Ausstehende Zusammenführung mit Kartenumsatz.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `Der Beleg kann aufgrund einer fehlerhaften Kartenverbindung nicht automatisch zugeordnet werden. Markieren Sie ihn als Bargeld, um ihn zu ignorieren, oder korrigieren Sie die Kartenverbindung, damit er zum Beleg passt.` + : 'Automatischer Abgleich des Belegs aufgrund einer unterbrochenen Kartenverbindung nicht möglich.'; + } return ''; }, brokenConnection530Error: 'Beleg ausstehend aufgrund einer unterbrochenen Bankverbindung', diff --git a/src/languages/es.ts b/src/languages/es.ts index b0d4886fae93a..cb55ee10c36fb 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3,6 +3,7 @@ import dedent from '@libs/StringUtils/dedent'; import CONST from '@src/CONST'; import type en from './en'; import type { + ConciergeBrokenCardConnectionParams, CreatedReportForUnapprovedTransactionsParams, HarvestCreatedExpenseReportParams, PaidElsewhereParams, @@ -1775,6 +1776,11 @@ const translations: TranslationDeepObject = { password: 'Por favor, introduce tu contraseña de Expensify', }, }, + personalCard: { + brokenConnection: 'La conexión de tu tarjeta está rota', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `La conexión de tu tarjeta ${cardName} está interrumpida. Inicia sesión en tu banco para reparar la tarjeta.`, + }, walletPage: { balance: 'Saldo', paymentMethodsTitle: 'Métodos de pago', @@ -7358,7 +7364,7 @@ ${amount} para ${merchant} - ${date}`, }, customRules: ({message}) => message, reviewRequired: 'Revisión requerida', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { 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.'; } @@ -7372,6 +7378,11 @@ ${amount} para ${merchant} - ${date}`, ? `Pide a ${member} que marque la transacción como efectivo o espera 7 días e inténtalo de nuevo` : 'Esperando a adjuntar automáticamente la transacción de tarjeta de crédito'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `No se puede vincular automáticamente el recibo debido a una conexión de tarjeta defectuosa. Márquelo como efectivo para ignorarlo o arregle la tarjeta para que coincida con el recibo.` + : 'No se puede hacer coincidir automáticamente el recibo debido a una conexión de tarjeta rota.'; + } return ''; }, brokenConnection530Error: 'Recibo pendiente debido a una conexión bancaria rota', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 9ce5c95e50243..bd7569477cfb2 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -20,6 +20,7 @@ import type OriginalMessage from '@src/types/onyx/OriginalMessage'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2078,6 +2079,11 @@ const translations: TranslationDeepObject = { password: 'Veuillez saisir votre mot de passe Expensify', }, }, + personalCard: { + brokenConnection: 'La connexion de votre carte est interrompue.', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `La connexion à votre carte ${cardName} est interrompue. Connectez-vous à votre banque pour rétablir la connexion.`, + }, walletPage: { balance: 'Solde', paymentMethodsTitle: 'Modes de paiement', @@ -7290,7 +7296,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Révision requise', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Impossible de faire correspondre automatiquement le reçu en raison d’une connexion bancaire défectueuse'; } @@ -7302,6 +7308,11 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin if (!isTransactionOlderThan7Days) { return isAdmin ? `Demandez à ${member} de marquer comme espèce ou attendez 7 jours et réessayez` : 'En attente de fusion avec la transaction par carte.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `Impossible de faire correspondre automatiquement le reçu en raison d'une connexion carte défaillante. Marquez-le comme paiement en espèces pour l'ignorer, ou réparez la carte pour faire correspondre le reçu.` + : "Impossible de faire correspondre automatiquement le reçu en raison d'une connexion carte défaillante."; + } return ''; }, brokenConnection530Error: 'Reçu en attente en raison d’une connexion bancaire rompue', diff --git a/src/languages/it.ts b/src/languages/it.ts index 82976e01d043a..465a42250a28a 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -20,6 +20,7 @@ import type OriginalMessage from '@src/types/onyx/OriginalMessage'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2068,6 +2069,11 @@ const translations: TranslationDeepObject = { password: 'Inserisci la tua password Expensify', }, }, + personalCard: { + brokenConnection: 'La connessione della tua scheda è interrotta', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `La connessione alla tua carta ${cardName} è interrotta. Accedi alla tua banca per riparare la carta.`, + }, walletPage: { balance: 'Saldo', paymentMethodsTitle: 'Metodi di pagamento', @@ -7265,7 +7271,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Revisione richiesta', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Impossibile abbinare automaticamente la ricevuta a causa di una connessione bancaria interrotta'; } @@ -7277,6 +7283,11 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori if (!isTransactionOlderThan7Days) { return isAdmin ? `Chiedi a ${member} di contrassegnarlo come contante oppure attendi 7 giorni e riprova` : 'In attesa dell’unione con la transazione della carta.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `Impossibile associare automaticamente la ricevuta a causa di una connessione interrotta con la carta. Contrassegna come contanti per ignorare o correggi la carta per farla corrispondere alla ricevuta.` + : 'Impossibile abbinare automaticamente la ricevuta a causa di una connessione interrotta con la carta.'; + } return ''; }, brokenConnection530Error: 'Ricevuta in sospeso a causa di connessione bancaria interrotta', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 79461097e2661..6afaeceef8573 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -20,6 +20,7 @@ import type OriginalMessage from '@src/types/onyx/OriginalMessage'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2065,6 +2066,11 @@ const translations: TranslationDeepObject = { password: 'Expensify のパスワードを入力してください', }, }, + personalCard: { + brokenConnection: 'カード接続が切断されました', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `${cardName} カードの接続が切断されました。銀行にログインしてカードを修復してください。`, + }, walletPage: { balance: '残高', paymentMethodsTitle: '支払方法', @@ -7209,7 +7215,7 @@ ${reportName} }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'レビューが必要', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return '銀行連携の不具合により、領収書を自動照合できません'; } @@ -7221,6 +7227,11 @@ ${reportName} if (!isTransactionOlderThan7Days) { return isAdmin ? `${member} に現金としてマークするよう依頼するか、7日間待ってからもう一度お試しください` : 'カード取引との統合を待機中です。'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `カード接続が切断されているため、レシートを自動照合できません。現金としてマークして無視するか、レシートと一致するようにカードを修正してください。` + : 'カード接続が切断されているため、領収書を自動照合できません。'; + } return ''; }, brokenConnection530Error: '銀行接続の不具合により領収書が保留中', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 513d2e231b69a..1900b22939caf 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -20,6 +20,7 @@ import type OriginalMessage from '@src/types/onyx/OriginalMessage'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2066,6 +2067,11 @@ const translations: TranslationDeepObject = { password: 'Voer uw Expensify-wachtwoord in', }, }, + personalCard: { + brokenConnection: 'De verbinding met uw netwerkkaart is verbroken.', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `De verbinding met uw kaart ${cardName} is verbroken. Log in bij uw bank om de kaart te herstellen.`, + }, walletPage: { balance: 'Saldo', paymentMethodsTitle: 'Betaalmethoden', @@ -7253,7 +7259,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Beoordeling vereist', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Kan bon niet automatisch koppelen vanwege verbroken bankverbinding'; } @@ -7265,6 +7271,11 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten if (!isTransactionOlderThan7Days) { return isAdmin ? `Vraag ${member} om het als contant te markeren of wacht 7 dagen en probeer het opnieuw` : 'In afwachting van samenvoeging met kaarttransactie.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `Automatische koppeling van bon mislukt vanwege verbroken kaartverbinding. Markeer als contant om te negeren, of corrigeer de kaart zodat deze overeenkomt met de bon.` + : 'Bon kan niet automatisch worden gekoppeld vanwege een onderbroken kaartverbinding.'; + } return ''; }, brokenConnection530Error: 'Bon in behandeling vanwege verbroken bankverbinding', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 7dce1e54dd103..6de356288e552 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -20,6 +20,7 @@ import type OriginalMessage from '@src/types/onyx/OriginalMessage'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2063,6 +2064,11 @@ const translations: TranslationDeepObject = { password: 'Wprowadź swoje hasło do Expensify', }, }, + personalCard: { + brokenConnection: 'Połączenie z Twoją kartą jest zerwane', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `Połączenie z Twoją kartą ${cardName} jest zerwane. Zaloguj się do swojego banku, aby naprawić kartę.`, + }, walletPage: { balance: 'Saldo', paymentMethodsTitle: 'Metody płatności', @@ -7238,7 +7244,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Wymagana weryfikacja', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Nie można automatycznie dopasować paragonu z powodu zerwanego połączenia z bankiem'; } @@ -7250,6 +7256,11 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i if (!isTransactionOlderThan7Days) { return isAdmin ? `Poproś ${member}, aby oznaczył jako gotówkę lub poczekaj 7 dni i spróbuj ponownie` : 'Oczekiwanie na połączenie z transakcją z karty.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `Nie można automatycznie dopasować paragonu z powodu zerwanego połączenia z kartą. Oznacz jako gotówkę, aby zignorować, lub napraw kartę, aby dopasować ją do paragonu.` + : 'Nie można automatycznie dopasować paragonu z powodu uszkodzonego połączenia karty.'; + } return ''; }, brokenConnection530Error: 'Paragon oczekujący z powodu zerwanego połączenia z bankiem', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index aa479edd54fb8..25710134b976f 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -20,6 +20,7 @@ import type OriginalMessage from '@src/types/onyx/OriginalMessage'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2063,6 +2064,11 @@ const translations: TranslationDeepObject = { password: 'Insira sua senha do Expensify', }, }, + personalCard: { + brokenConnection: 'A conexão do seu cartão está interrompida', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `A conexão do seu cartão ${cardName} está interrompida. Faça login no seu banco para corrigir o cartão.`, + }, walletPage: { balance: 'Saldo', paymentMethodsTitle: 'Formas de pagamento', @@ -7242,7 +7248,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Revisão necessária', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Não é possível corresponder o recibo automaticamente devido a uma conexão bancária com problemas'; } @@ -7254,6 +7260,11 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe if (!isTransactionOlderThan7Days) { return isAdmin ? `Peça para ${member} marcar como dinheiro em espécie ou aguarde 7 dias e tente novamente` : 'Aguardando combinação com a transação do cartão.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `Não foi possível associar o recibo automaticamente devido a uma falha na conexão do cartão. Marque como dinheiro para ignorar ou corrija o cartão para que a correspondência com o recibo seja feita.` + : 'Não foi possível associar o recibo automaticamente devido a uma falha na conexão do cartão.'; + } return ''; }, brokenConnection530Error: 'Recibo pendente devido à conexão bancária quebrada', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index bf8c9f832080e..00a1046ed2cc8 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -20,6 +20,7 @@ import type OriginalMessage from '@src/types/onyx/OriginalMessage'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2036,6 +2037,11 @@ const translations: TranslationDeepObject = { password: '请输入您的 Expensify 密码', }, }, + personalCard: { + brokenConnection: '您的卡连接已断开', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `您的 ${cardName} 卡连接已断开。登录您的银行 以修复卡片。`, + }, walletPage: { balance: '余额', paymentMethodsTitle: '付款方式', @@ -7090,7 +7096,7 @@ ${reportName} }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: '需要审核', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return '由于银行连接中断,无法自动匹配收据'; } @@ -7100,6 +7106,11 @@ ${reportName} if (!isTransactionOlderThan7Days) { return isAdmin ? `请让 ${member} 标记为现金,或等待 7 天后再试` : '正在等待与卡片交易合并。'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `由于卡片连接故障,无法自动匹配收据。请将其标记为现金以忽略,或修复卡片以匹配收据。` + : '由于卡片连接故障,无法自动匹配收据。'; + } return ''; }, brokenConnection530Error: '由于银行连接中断,收据正在等待处理中', diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index a09cc8165d7ae..d7642f57b167f 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -487,7 +487,14 @@ const ViolationsUtils = { * possible values could be either translation keys that resolve to strings or translation keys that resolve to * functions. */ - getViolationTranslation(violation: TransactionViolation, translate: LocaleContextProps['translate'], canEdit = true, tags?: PolicyTagLists, companyCardPageURL?: string): string { + getViolationTranslation( + violation: TransactionViolation, + translate: LocaleContextProps['translate'], + canEdit = true, + tags?: PolicyTagLists, + companyCardPageURL?: string, + connectionLink?: string, + ): string { const { brokenBankConnection = false, isAdmin = false, @@ -569,6 +576,7 @@ const ViolationsUtils = { member, rterType, companyCardPageURL, + connectionLink, }); case 'smartscanFailed': return translate('violations.smartscanFailed', {canEdit}); @@ -616,6 +624,7 @@ const ViolationsUtils = { transactionThreadActions?: ReportAction[], tags?: PolicyTagLists, companyCardPageURL?: string, + connectionLink?: string, ): string { const errorMessages = extractErrorMessages(transaction?.errors ?? {}, transactionThreadActions?.filter((e) => !!e.errors) ?? [], translate); @@ -625,7 +634,7 @@ const ViolationsUtils = { // Some violations end with a period already so lets make sure the connected messages have only single period between them // and end with a single dot. ...transactionViolations.map((violation) => { - const message = ViolationsUtils.getViolationTranslation(violation, translate, true, tags, companyCardPageURL); + const message = ViolationsUtils.getViolationTranslation(violation, translate, true, tags, companyCardPageURL, connectionLink); if (!message) { return; } From fb35c9571f058fa92433a929f44c2ebead6ab5a4 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Wed, 21 Jan 2026 19:02:35 +0200 Subject: [PATCH 3/7] fix lints --- src/components/ReportActionItem/MoneyRequestView.tsx | 3 ++- src/languages/es.ts | 9 ++++++++- src/pages/home/report/PureReportActionItem.tsx | 3 +-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 358054d2793c8..c2c46e04c0122 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -58,7 +58,8 @@ import {isSplitAction} from '@libs/ReportSecondaryActionUtils'; import { canEditFieldOfMoneyRequest, canEditMoneyRequest, - canUserPerformWriteAction as canUserPerformWriteActionReportUtils, // eslint-disable-next-line @typescript-eslint/no-deprecated + canUserPerformWriteAction as canUserPerformWriteActionReportUtils, + // eslint-disable-next-line @typescript-eslint/no-deprecated getReportName, getTransactionDetails, getTripIDFromTransactionParentReportID, diff --git a/src/languages/es.ts b/src/languages/es.ts index 0c06959295889..126d521da8d94 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2,7 +2,14 @@ import {CONST as COMMON_CONST} from 'expensify-common'; import dedent from '@libs/StringUtils/dedent'; import CONST from '@src/CONST'; import type en from './en'; -import type {ConciergeBrokenCardConnectionParams, CreatedReportForUnapprovedTransactionsParams, PaidElsewhereParams, RoutedDueToDEWParams, SplitDateRangeParams, ViolationsRterParams} from './params'; +import type { + ConciergeBrokenCardConnectionParams, + CreatedReportForUnapprovedTransactionsParams, + PaidElsewhereParams, + RoutedDueToDEWParams, + SplitDateRangeParams, + ViolationsRterParams, +} from './params'; import type {TranslationDeepObject} from './types'; /* eslint-disable max-len */ diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 8170779f5c192..116dea2be831d 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -215,7 +215,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -import type {CardList} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {JoinWorkspaceResolution, OriginalMessageMovedTransaction, OriginalMessageUnreportedTransaction} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject, isEmptyValueObject} from '@src/types/utils/EmptyObject'; @@ -357,7 +356,7 @@ type PureReportActionItemProps = { isChronosReport?: boolean; /** All cards */ - cardList?: CardList; + cardList?: OnyxTypes.CardList; /** Function to toggle emoji reaction */ toggleEmojiReaction?: ( From 16ac85fb313d324a1d9ac2528c1b201abad66a2d Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Thu, 29 Jan 2026 16:10:46 +0200 Subject: [PATCH 4/7] change naming --- src/CONST/index.ts | 2 +- src/libs/ReportActionsUtils.ts | 4 ++-- src/pages/inbox/report/PureReportActionItem.tsx | 2 +- src/pages/inbox/report/ReportActionItem.tsx | 2 +- src/types/onyx/OriginalMessage.ts | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 7b05e920f4595..dd99c31963ee7 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1238,7 +1238,7 @@ const CONST = { CARD_REPLACED_VIRTUAL: 'CARDREPLACEDVIRTUAL', CARD_REPLACED: 'CARDREPLACED', CARD_ASSIGNED: 'CARDASSIGNED', - CARD_CONNECTION_BROKEN: 'CARDCONNECTIONBROKEN', + PERSONAL_CARD_CONNECTION_BROKEN: 'PERSONALCARDCONNECTIONBROKEN', CHANGE_FIELD: 'CHANGEFIELD', // OldDot Action CHANGE_POLICY: 'CHANGEPOLICY', CREATED_REPORT_FOR_UNAPPROVED_TRANSACTIONS: 'CREATEDREPORTFORUNAPPROVEDTRANSACTIONS', diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index fa1a58675a24b..24180bcca9c32 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -376,8 +376,8 @@ function isHoldAction(reportAction: OnyxInputOrEntry): reportActio return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.HOLD); } -function isCardBrokenConnectionAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction { - return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.CARD_CONNECTION_BROKEN); +function isCardBrokenConnectionAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction { + return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.PERSONAL_CARD_CONNECTION_BROKEN); } function isReimbursementDirectionInformationRequiredAction( diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 4b77b9b92a376..ac12d5817bfd9 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -70,9 +70,9 @@ import { getActionableMentionWhisperMessage, getAddedApprovalRuleMessage, getAddedConnectionMessage, - getCardConnectionBrokenMessage, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, + getCardConnectionBrokenMessage, getChangedApproverActionMessage, getCompanyAddressUpdateMessage, getCompanyCardConnectionBrokenMessage, diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index 4864a4daaa7d6..40d5a20e84220 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -1,3 +1,4 @@ +import {filterPersonalCards} from '@selectors/Card'; import React from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useBlockedFromConcierge} from '@components/OnyxListItemProvider'; @@ -7,7 +8,6 @@ import useOnyx from '@hooks/useOnyx'; import useOriginalReportID from '@hooks/useOriginalReportID'; import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; import useReportIsArchived from '@hooks/useReportIsArchived'; -import {filterPersonalCards} from '@libs/CardUtils'; import {getForReportAction, getMovedReportID} from '@libs/ModifiedExpenseMessage'; import {getIOUReportIDFromReportActionPreview, getOriginalMessage} from '@libs/ReportActionsUtils'; import { diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index a142af7b5707d..4a167bd262f32 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -1122,7 +1122,7 @@ type OriginalMessageCard = { }; /** - * Model of CARD_CONNECTION_BROKEN action + * Model of PERSONAL_CARD_CONNECTION_BROKEN action */ type OriginalPersonalCard = { /** The id of the user the card was assigned to */ @@ -1269,7 +1269,7 @@ type OriginalMessageMap = { [CONST.REPORT.ACTIONS.TYPE.CARD_REPLACED_VIRTUAL]: OriginalMessageCard; [CONST.REPORT.ACTIONS.TYPE.CARD_REPLACED]: OriginalMessageCard; [CONST.REPORT.ACTIONS.TYPE.CARD_ASSIGNED]: OriginalMessageCard; - [CONST.REPORT.ACTIONS.TYPE.CARD_CONNECTION_BROKEN]: OriginalPersonalCard; + [CONST.REPORT.ACTIONS.TYPE.PERSONAL_CARD_CONNECTION_BROKEN]: OriginalPersonalCard; [CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED]: OriginalMessageIntegrationSyncFailed; [CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION]: OriginalMessageDeletedTransaction; [CONST.REPORT.ACTIONS.TYPE.DEW_SUBMIT_FAILED]: OriginalMessageDEWFailed; From 8d0c6b3590e0f21f7bc95c5eab518d5b10f0b2e9 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 29 Jan 2026 17:31:48 -0800 Subject: [PATCH 5/7] add cardID --- src/CONST/index.ts | 2 +- .../MoneyRequestReceiptView.tsx | 10 ++++++---- .../ReportActionItem/MoneyRequestView.tsx | 3 ++- .../TransactionPreviewContent.tsx | 5 ++++- .../TransactionItemRowRBR.tsx | 2 ++ src/components/ViolationMessages.tsx | 11 +++++++++-- src/languages/en.ts | 13 ++++++------- src/languages/params.ts | 1 + src/libs/Violations/ViolationsUtils.ts | 17 ++++++++++++++--- src/types/onyx/TransactionViolation.ts | 3 +++ 10 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index dd99c31963ee7..91ba14106a2d5 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -5900,7 +5900,7 @@ const CONST = { }, RTER_VIOLATION_TYPES: { BROKEN_CARD_CONNECTION: 'brokenCardConnection', - BROKEN_PERSONAL_CARD_CONNECTION: 'brokenPersonalCardConnection', + BROKEN_PERSONAL_CARD_CONNECTION: 'brokenCardConnection', BROKEN_CARD_CONNECTION_530: 'brokenCardConnection530', SEVEN_DAY_HOLD: 'sevenDayHold', }, diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index df5ff9da56146..3cc9be4d9f65e 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -121,6 +121,7 @@ function MoneyRequestReceiptView({ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(linkedTransactionID)}`, {canBeMissing: true}); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${moneyRequestReport?.policyID}`, {canBeMissing: true}); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const transactionViolations = useTransactionViolations(transaction?.transactionID); const isDistanceRequest = isDistanceRequestTransactionUtils(transaction); @@ -174,16 +175,17 @@ function MoneyRequestReceiptView({ for (const violation of filteredViolations) { const isReceiptFieldViolation = receiptFieldViolationNames.has(violation.name); const isReceiptImageViolation = receiptImageViolationNames.has(violation.name); - if (isReceiptFieldViolation || isReceiptImageViolation) { - const violationMessage = ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL, connectionLink); + const isRTERViolation = violation.name === CONST.VIOLATIONS.RTER; + if (isReceiptFieldViolation || isReceiptImageViolation || isRTERViolation) { + const violationMessage = ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL, connectionLink, cardList); allViolations.push(violationMessage); - if (isReceiptImageViolation) { + if (isReceiptImageViolation || isRTERViolation) { imageViolations.push(violationMessage); } } } return [imageViolations, allViolations]; - }, [transactionViolations, translate, canEdit, companyCardPageURL, connectionLink]); + }, [transactionViolations, translate, canEdit, companyCardPageURL, connectionLink, cardList]); const receiptRequiredViolation = transactionViolations?.some((violation) => violation.name === CONST.VIOLATIONS.RECEIPT_REQUIRED); const itemizedReceiptRequiredViolation = transactionViolations?.some((violation) => violation.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 70b50b5f2db9e..157bdbb2742bd 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -232,6 +232,7 @@ function MoneyRequestView({ const allPolicyTags = usePolicyTags(); const policyTagList = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetPolicyID}`]; const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${getNonEmptyStringOnyxID(linkedTransactionID)}`, {canBeMissing: true}); const transactionViolations = useTransactionViolations(transaction?.transactionID); @@ -570,7 +571,7 @@ function MoneyRequestView({ // Return violations if there are any if (field !== 'merchant' && hasViolations(field, data, policyHasDependentTags, tagValue)) { const violations = getViolationsForField(field, data, policyHasDependentTags, tagValue); - return `${violations.map((violation) => ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL, connectionLink)).join('. ')}.`; + return `${violations.map((violation) => ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL, connectionLink, cardList)).join('. ')}.`; } if (field === 'attendees' && isMissingAttendeesViolation) { diff --git a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx index aa06772bbf64d..3d073878cc2a7 100644 --- a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx +++ b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx @@ -77,6 +77,7 @@ function TransactionPreviewContent({ ); const {amount, comment: requestComment, merchant, tag, category, currency: requestCurrency} = transactionDetails; const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transaction?.comment?.originalTransactionID)}`, {canBeMissing: true}); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const managerID = report?.managerID ?? reportPreviewAction?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; const ownerAccountID = report?.ownerAccountID ?? reportPreviewAction?.childOwnerAccountID ?? CONST.DEFAULT_NUMBER_ID; @@ -120,7 +121,9 @@ function TransactionPreviewContent({ const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(report?.policyID)}`; // TODO add correct link to card page in wallet settings const connectionLink = `${environmentURL}/${ROUTES.SETTINGS_WALLET}`; - const violationMessage = firstViolation ? ViolationsUtils.getViolationTranslation(firstViolation, translate, canEdit, undefined, companyCardPageURL, connectionLink) : undefined; + const violationMessage = firstViolation + ? ViolationsUtils.getViolationTranslation(firstViolation, translate, canEdit, undefined, companyCardPageURL, connectionLink, cardList) + : undefined; const previewText = useMemo( () => diff --git a/src/components/TransactionItemRow/TransactionItemRowRBR.tsx b/src/components/TransactionItemRow/TransactionItemRowRBR.tsx index c0b8813543d30..b11cc9888aa01 100644 --- a/src/components/TransactionItemRow/TransactionItemRowRBR.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowRBR.tsx @@ -47,6 +47,7 @@ function TransactionItemRowRBR({transaction, violations, report, containerStyles // TODO add correct link to card page in wallet settings const connectionLink = `${environmentURL}/${ROUTES.SETTINGS_WALLET}`; const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: true}); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const transactionThreadId = reportActions ? getIOUActionForTransactionID(Object.values(reportActions ?? {}), transaction.transactionID)?.childReportID : undefined; const [transactionThreadActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadId}`, { canBeMissing: true, @@ -61,6 +62,7 @@ function TransactionItemRowRBR({transaction, violations, report, containerStyles policyTags, companyCardPageURL, connectionLink, + cardList, ); return ( diff --git a/src/components/ViolationMessages.tsx b/src/components/ViolationMessages.tsx index 636bbea0c1ca3..4dc404c628763 100644 --- a/src/components/ViolationMessages.tsx +++ b/src/components/ViolationMessages.tsx @@ -2,8 +2,10 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import ViolationsUtils, {filterReceiptViolations} from '@libs/Violations/ViolationsUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {TransactionViolation} from '@src/types/onyx'; import Text from './Text'; @@ -20,12 +22,17 @@ type ViolationMessagesProps = { export default function ViolationMessages({violations, isLast, containerStyle, textStyle, canEdit, companyCardPageURL, connectionLink}: ViolationMessagesProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const filteredViolations = useMemo(() => filterReceiptViolations(violations), [violations]); const violationMessages = useMemo( - () => filteredViolations.map((violation) => [violation.name, ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL, connectionLink)]), - [canEdit, translate, filteredViolations, companyCardPageURL, connectionLink], + () => + filteredViolations.map((violation) => [ + violation.name, + ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL, connectionLink, cardList), + ]), + [canEdit, translate, filteredViolations, companyCardPageURL, connectionLink, cardList], ); return ( diff --git a/src/languages/en.ts b/src/languages/en.ts index 5b9ee4a1657ca..966a34d383f56 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7440,10 +7440,15 @@ const translations = { }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Review required', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink, isPersonalCard}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return "Can't auto-match receipt due to broken bank connection"; } + if (isPersonalCard && (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || brokenBankConnection)) { + return isAdmin + ? `Can't auto-match receipt due to broken card connection. Mark as cash to ignore, or fix the card to match the receipt.` + : "Can't auto-match receipt due to broken card connection."; + } if (brokenBankConnection || rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION) { return isAdmin ? `Bank connection broken. Reconnect to match receipt` @@ -7453,12 +7458,6 @@ const translations = { return isAdmin ? `Ask ${member} to mark as a cash or wait 7 days and try again` : 'Awaiting merge with card transaction.'; } - if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { - return isAdmin - ? `Can’t auto-match receipt due to broken card connection. Mark as cash to ignore, or fix the card to match the receipt.` - : "Can't auto-match receipt due to broken card connection."; - } - return ''; }, brokenConnection530Error: 'Receipt pending due to broken bank connection', diff --git a/src/languages/params.ts b/src/languages/params.ts index 70e2325c656dc..9a83d54134d9d 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -228,6 +228,7 @@ type ViolationsRterParams = { rterType?: ValueOf; companyCardPageURL?: string; connectionLink?: string; + isPersonalCard?: boolean; }; type ViolationsTagOutOfPolicyParams = {tagName?: string} | undefined; diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 4608a313b00c8..d2a207df885f2 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -5,6 +5,7 @@ import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import {getIsMissingAttendeesViolation} from '@libs/AttendeeUtils'; +import {isPersonalCard} from '@libs/CardUtils'; import {getDecodedCategoryName, isCategoryMissing} from '@libs/CategoryUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; @@ -17,7 +18,7 @@ import * as TransactionUtils from '@libs/TransactionUtils'; import {hasValidModifiedAmount, isViolationDismissed, shouldShowViolation} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyCategories, PolicyTagLists, Report, ReportAction, Transaction, TransactionViolation, ViolationName} from '@src/types/onyx'; +import type {CardList, Policy, PolicyCategories, PolicyTagLists, Report, ReportAction, Transaction, TransactionViolation, ViolationName} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {ReceiptError, ReceiptErrors} from '@src/types/onyx/Transaction'; import type ViolationFixParams from './types'; @@ -644,6 +645,7 @@ const ViolationsUtils = { tags?: PolicyTagLists, companyCardPageURL?: string, connectionLink?: string, + cardList?: CardList, ): string { const { brokenBankConnection = false, @@ -659,6 +661,7 @@ const ViolationsUtils = { taxName, type, rterType, + cardID, message = '', errorIndexes = [], } = violation.data ?? {}; @@ -722,7 +725,12 @@ const ViolationsUtils = { return translate('violations.itemizedReceiptRequired', {formattedLimit}); case 'customRules': return translate('violations.customRules', {message}); - case 'rter': + case 'rter': { + let isPersonalCardViolation = false; + if (cardID !== undefined && cardID !== null && cardList) { + const card = cardList[cardID]; + isPersonalCardViolation = !!isPersonalCard(card); + } return translate('violations.rter', { brokenBankConnection, isAdmin, @@ -731,7 +739,9 @@ const ViolationsUtils = { rterType, companyCardPageURL, connectionLink, + isPersonalCard: isPersonalCardViolation, }); + } case 'smartscanFailed': return translate('violations.smartscanFailed', {canEdit}); case 'someTagLevelsRequired': @@ -781,6 +791,7 @@ const ViolationsUtils = { tags?: PolicyTagLists, companyCardPageURL?: string, connectionLink?: string, + cardList?: CardList, ): string { const errorMessages = extractErrorMessages(transaction?.errors ?? {}, transactionThreadActions?.filter((e) => !!e.errors) ?? [], translate); const filteredViolations = filterReceiptViolations(transactionViolations); @@ -791,7 +802,7 @@ const ViolationsUtils = { // Some violations end with a period already so lets make sure the connected messages have only single period between them // and end with a single dot. ...filteredViolations.map((violation) => { - const message = ViolationsUtils.getViolationTranslation(violation, translate, true, tags, companyCardPageURL, connectionLink); + const message = ViolationsUtils.getViolationTranslation(violation, translate, true, tags, companyCardPageURL, connectionLink, cardList); if (!message) { return; } diff --git a/src/types/onyx/TransactionViolation.ts b/src/types/onyx/TransactionViolation.ts index 76b6e9410d68d..88d565bdd43ae 100644 --- a/src/types/onyx/TransactionViolation.ts +++ b/src/types/onyx/TransactionViolation.ts @@ -103,6 +103,9 @@ type TransactionViolationData = { /** Comment that triggered the violation */ comment?: string; + + /** Card ID associated with the violation (used to determine if it's a personal or company card) */ + cardID?: number; }; /** Model of a transaction violation */ From 881c397c3237ed16f1d6bdd6e87182f029f69353 Mon Sep 17 00:00:00 2001 From: Test Date: Fri, 30 Jan 2026 13:02:20 -0800 Subject: [PATCH 6/7] fix concierge error and add personal card broken error --- .../MoneyRequestReceiptView.tsx | 3 +- .../ReportActionItem/MoneyRequestView.tsx | 3 +- .../TransactionPreviewContent.tsx | 3 +- .../TransactionItemRowRBR.tsx | 3 +- src/languages/en.ts | 1 + src/libs/DebugUtils.ts | 1 + src/libs/ReportActionsUtils.ts | 7 ++-- .../inbox/report/PureReportActionItem.tsx | 10 ++++- src/pages/inbox/report/ReportActionItem.tsx | 4 +- .../Wallet/PersonalCardDetailsPage.tsx | 37 ++++++++++++++++++- 10 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index 3cc9be4d9f65e..27bf154f5c3e2 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -137,8 +137,7 @@ function MoneyRequestReceiptView({ const isEditable = !!canUserPerformWriteActionReportUtils(report, isReportArchived) && !readonly; const canEdit = isMoneyRequestAction(parentReportAction) && canEditMoneyRequest(parentReportAction, isChatReportArchived, moneyRequestReport, policy, transaction) && isEditable; const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(report?.policyID)}`; - // TODO add correct link to card page in wallet settings - const connectionLink = `${environmentURL}/${ROUTES.SETTINGS_WALLET}`; + const connectionLink = `${environmentURL}/${ROUTES.SETTINGS_WALLET_PERSONAL_CARD_DETAILS.getRoute(String(transaction?.cardID ?? ''))}`; const canEditReceipt = isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT, undefined, isChatReportArchived, undefined, transaction, moneyRequestReport, policy); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 157bdbb2742bd..fbcb3d90f3b1d 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -328,8 +328,7 @@ function MoneyRequestView({ const isEditable = !!canUserPerformWriteActionReportUtils(transactionThreadReport, isReportArchived) && !readonly; const canEdit = isMoneyRequestAction(parentReportAction) && canEditMoneyRequest(parentReportAction, isChatReportArchived, moneyRequestReport, policy, transaction) && isEditable; const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(transactionThreadReport?.policyID)}`; - // TODO add correct link to card page in wallet settings - const connectionLink = `${environmentURL}/${ROUTES.SETTINGS_WALLET}`; + const connectionLink = `${environmentURL}/${ROUTES.SETTINGS_WALLET_PERSONAL_CARD_DETAILS.getRoute(String(transaction?.cardID ?? ''))}`; const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transaction?.comment?.originalTransactionID)}`, {canBeMissing: true}); const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction); const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`, {canBeMissing: true}); diff --git a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx index 3d073878cc2a7..2740271edb015 100644 --- a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx +++ b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx @@ -119,8 +119,7 @@ function TransactionPreviewContent({ const isIOUActionType = isMoneyRequestAction(action); const canEdit = isIOUActionType && canEditMoneyRequest(action, isChatReportArchived, report, policy, transaction); const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(report?.policyID)}`; - // TODO add correct link to card page in wallet settings - const connectionLink = `${environmentURL}/${ROUTES.SETTINGS_WALLET}`; + const connectionLink = `${environmentURL}/${ROUTES.SETTINGS_WALLET_PERSONAL_CARD_DETAILS.getRoute(String(transaction?.cardID ?? ''))}`; const violationMessage = firstViolation ? ViolationsUtils.getViolationTranslation(firstViolation, translate, canEdit, undefined, companyCardPageURL, connectionLink, cardList) : undefined; diff --git a/src/components/TransactionItemRow/TransactionItemRowRBR.tsx b/src/components/TransactionItemRow/TransactionItemRowRBR.tsx index b11cc9888aa01..9226362bdcadb 100644 --- a/src/components/TransactionItemRow/TransactionItemRowRBR.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowRBR.tsx @@ -44,8 +44,7 @@ function TransactionItemRowRBR({transaction, violations, report, containerStyles canBeMissing: true, }); const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(report?.policyID)}`; - // TODO add correct link to card page in wallet settings - const connectionLink = `${environmentURL}/${ROUTES.SETTINGS_WALLET}`; + const connectionLink = `${environmentURL}/${ROUTES.SETTINGS_WALLET_PERSONAL_CARD_DETAILS.getRoute(String(transaction?.cardID ?? ''))}`; const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: true}); const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const transactionThreadId = reportActions ? getIOUActionForTransactionID(Object.values(reportActions ?? {}), transaction.transactionID)?.childReportID : undefined; diff --git a/src/languages/en.ts b/src/languages/en.ts index 1828ed84a772d..14647b7ec8602 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2173,6 +2173,7 @@ const translations = { }, personalCard: { brokenConnection: 'Your card connection is broken', + fixCard: 'Fix card', conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => `Your ${cardName} card connection is broken. Log into your bank to fix the card.`, }, diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 8d7af62b6891c..f980d97416432 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -1320,6 +1320,7 @@ function validateTransactionViolationDraftProperty(key: keyof TransactionViolati field: 'string', prohibitedExpenseRule: 'string', comment: 'string', + cardID: 'number', }); case 'showInReview': return validateBoolean(value); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index c0927096bd44d..4a952b7ef5733 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3686,14 +3686,13 @@ function getCardIssuedMessage({ } } -function getCardConnectionBrokenMessage(reportAction: OnyxEntry, card: Card | undefined, translate: LocaleContextProps['translate']) { +function getCardConnectionBrokenMessage(reportAction: OnyxEntry, card: Card | undefined, translate: LocaleContextProps['translate'], connectionLink: string) { if (!isCardBrokenConnectionAction(reportAction) || !isPersonalCardBrokenConnection(card)) { return ''; } const cardName = card?.cardName; - const isPlaid = !!getPlaidInstitutionId(card?.bank); - const personalCardName = isPlaid && cardName ? cardName : getBankName(card?.bank as CompanyCardFeed); - return translate('personalCard.conciergeBrokenConnection', {cardName: personalCardName, connectionLink: ''}); + const personalCardName = cardName ?? getBankName(card?.bank as CompanyCardFeed); + return translate('personalCard.conciergeBrokenConnection', {cardName: personalCardName, connectionLink}); } function getRoomChangeLogMessage(translate: LocalizedTranslate, reportAction: ReportAction) { diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 72192f83a1835..d5bb07510627d 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -42,6 +42,7 @@ import UnreadActionIndicator from '@components/UnreadActionIndicator'; import useActivePolicy from '@hooks/useActivePolicy'; import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useEnvironment from '@hooks/useEnvironment'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import usePreferredPolicy from '@hooks/usePreferredPolicy'; @@ -564,6 +565,7 @@ function PureReportActionItem({ const isOriginalReportArchived = useReportIsArchived(originalReportID); const isHarvestCreatedExpenseReport = isHarvestCreatedExpenseReportUtils(reportNameValuePairsOrigin, reportNameValuePairsOriginalID); const expensifyIcons = useMemoizedLazyExpensifyIcons(['Eye'] as const); + const {environmentURL} = useEnvironment(); const highlightedBackgroundColorIfNeeded = useMemo( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -1614,8 +1616,14 @@ function PureReportActionItem({ ); } else if (isCardBrokenConnectionAction(action)) { const cardID = getOriginalMessage(action)?.cardID; + console.log('cardID', cardID); const card = cardID ? cardList?.[cardID] : undefined; - children = ; + const connectionLink = cardID ? `${environmentURL}/${ROUTES.SETTINGS_WALLET_PERSONAL_CARD_DETAILS.getRoute(String(cardID))}` : ''; + children = ( + + ${getCardConnectionBrokenMessage(action, card, translate, connectionLink)}`} /> + + ); } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION)) { children = ; } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.RECEIPT_SCAN_FAILED)) { diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index 40d5a20e84220..2715d03185dd8 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -1,4 +1,4 @@ -import {filterPersonalCards} from '@selectors/Card'; +import {filterOutPersonalCards} from '@selectors/Card'; import React from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useBlockedFromConcierge} from '@components/OnyxListItemProvider'; @@ -108,7 +108,7 @@ function ReportActionItem({ const movedFromReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(action, CONST.REPORT.MOVE_TYPE.FROM)}`]; const movedToReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(action, CONST.REPORT.MOVE_TYPE.TO)}`]; const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; - const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterPersonalCards, canBeMissing: false}); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterOutPersonalCards, canBeMissing: true}); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true}); const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); diff --git a/src/pages/settings/Wallet/PersonalCardDetailsPage.tsx b/src/pages/settings/Wallet/PersonalCardDetailsPage.tsx index 2fbcac94fe39c..4b35852828a5d 100644 --- a/src/pages/settings/Wallet/PersonalCardDetailsPage.tsx +++ b/src/pages/settings/Wallet/PersonalCardDetailsPage.tsx @@ -1,9 +1,12 @@ import {format} from 'date-fns'; import React, {useState} from 'react'; import {View} from 'react-native'; +import Button from '@components/Button'; import ConfirmModal from '@components/ConfirmModal'; +import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ImageSVG from '@components/ImageSVG'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import {useCompanyCardFeedIcons} from '@hooks/useCompanyCardIcons'; @@ -13,14 +16,15 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getCardFeedIcon, isPersonalCard} from '@libs/CardUtils'; +import {getCardFeedIcon, isCardConnectionBroken, isPersonalCard} from '@libs/CardUtils'; +import {getLatestErrorField} from '@libs/ErrorUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; import Navigation from '@navigation/Navigation'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import variables from '@styles/variables'; -import {syncCard, unassignCard} from '@userActions/Card'; +import {clearCardErrorField, syncCard, unassignCard} from '@userActions/Card'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -48,6 +52,7 @@ function PersonalCardDetailsPage({route}: PersonalCardDetailsPageProps) { const card = cardList?.[cardID]; const cardBank = card?.bank ?? ''; + const isCardBroken = card ? isCardConnectionBroken(card) : false; const cardholder = personalDetails?.[card?.accountID ?? CONST.DEFAULT_NUMBER_ID]; const displayName = getDisplayNameOrDefault(cardholder); const isUserPersonalCard = !!(card && isPersonalCard(card)); @@ -108,6 +113,34 @@ function PersonalCardDetailsPage({route}: PersonalCardDetailsPageProps) { width={variables.cardPreviewWidth} /> + {isCardBroken && ( + { + if (!card) { + return; + } + clearCardErrorField(card.cardID, 'lastScrape'); + }} + > + + +