From 019c7bfbb3a7ce8bc06b23e2bd37a69252670135 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 4 May 2026 17:44:43 -0300 Subject: [PATCH 01/17] add policy change log --- src/CONST/index.ts | 3 + src/libs/ReportActionsUtils.ts | 219 ++++++++++++++++++ src/libs/SidebarUtils.ts | 9 + .../report/ContextMenu/ContextMenuActions.tsx | 9 + .../actionContents/PolicyChangeLogContent.tsx | 6 + src/types/onyx/OriginalMessage.ts | 66 ++++++ 6 files changed, 312 insertions(+) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 6cfe90209277..de2d57b12887 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1525,6 +1525,7 @@ const CONST = { ADD_CUSTOM_UNIT_RATE: 'POLICYCHANGELOG_ADD_CUSTOM_UNIT_RATE', ADD_EMPLOYEE: 'POLICYCHANGELOG_ADD_EMPLOYEE', ADD_CARD_FEED: 'POLICYCHANGELOG_ADD_CARD_FEED', + ADD_EXPENSIFY_CARD_RULE: 'POLICYCHANGELOG_ADD_EXPENSIFY_CARD_RULE', ADD_INTEGRATION: 'POLICYCHANGELOG_ADD_INTEGRATION', ADD_REPORT_FIELD: 'POLICYCHANGELOG_ADD_REPORT_FIELD', ADD_TAG: 'POLICYCHANGELOG_ADD_TAG', @@ -1550,6 +1551,7 @@ const CONST = { INDIVIDUAL_BUDGET_NOTIFICATION: 'POLICYCHANGELOG_INDIVIDUAL_BUDGET_NOTIFICATION', INVITE_TO_ROOM: 'POLICYCHANGELOG_INVITETOROOM', REMOVE_FROM_ROOM: 'POLICYCHANGELOG_REMOVEFROMROOM', + REMOVE_EXPENSIFY_CARD_RULE: 'POLICYCHANGELOG_REMOVE_EXPENSIFY_CARD_RULE', LEAVE_ROOM: 'POLICYCHANGELOG_LEAVEROOM', REPLACE_CATEGORIES: 'POLICYCHANGELOG_REPLACE_CATEGORIES', SET_AUTO_REIMBURSEMENT: 'POLICYCHANGELOG_SET_AUTOREIMBURSEMENT', @@ -1576,6 +1578,7 @@ const CONST = { UPDATE_DEFAULT_TITLE_ENFORCED: 'POLICYCHANGELOG_UPDATE_DEFAULT_TITLE_ENFORCED', UPDATE_DISABLED_FIELDS: 'POLICYCHANGELOG_UPDATE_DISABLED_FIELDS', UPDATE_EMPLOYEE: 'POLICYCHANGELOG_UPDATE_EMPLOYEE', + UPDATE_EXPENSIFY_CARD_RULE: 'POLICYCHANGELOG_UPDATE_EXPENSIFY_CARD_RULE', UPDATE_FIELD: 'POLICYCHANGELOG_UPDATE_FIELD', UPDATE_ADDRESS: 'POLICYCHANGELOG_UPDATE_ADDRESS', UPDATE_FEATURE_ENABLED: 'POLICYCHANGELOG_UPDATE_FEATURE_ENABLED', diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 0035ac6e13ec..33352b25e862 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -33,6 +33,7 @@ import type { OriginalMessageChangeLog, OriginalMessageExportIntegration, OriginalMessageMarkedReimbursed, + OriginalMessagePolicyChangeLog, OriginalMessageReimbursed, OriginalMessageUnreportedTransaction, PolicyBudgetFrequency, @@ -3905,6 +3906,221 @@ function getUpdatedApprovalRuleMessage(translate: LocalizedTranslate, reportActi return getReportActionText(reportAction); } +/** Mirrors Web-Expensify `Report_Action_PolicyChangeLog_SpendRuleMessage::preformattedText` */ +function getSpendRulePreformattedText(message: OriginalMessagePolicyChangeLog): string | undefined { + for (const key of ['changeLogText', 'text', 'displayMessage'] as const) { + const value = message?.[key]; + if (typeof value === 'string' && value !== '') { + return value; + } + } + return undefined; +} + +function cardCountFromSpendRuleMessage(message: OriginalMessagePolicyChangeLog): number { + const raw = message?.cardCount; + if (typeof raw === 'number' && Number.isFinite(raw)) { + return raw; + } + if (typeof raw === 'string' && raw !== '' && /^\d+$/.test(raw)) { + return Number.parseInt(raw, 10); + } + return 0; +} + +function spendRuleCardScopeFragment(message: OriginalMessagePolicyChangeLog): string { + const cardCount = cardCountFromSpendRuleMessage(message); + const cardName = typeof message?.cardName === 'string' ? message.cardName : ''; + if (cardCount > 1) { + return `${cardCount} cards`; + } + if (cardName !== '') { + return cardName; + } + if (cardCount === 1) { + return '1 card'; + } + let lastFour = typeof message?.cardLastFour === 'string' ? message.cardLastFour : ''; + if (lastFour === '' && typeof message?.lastFour === 'string') { + lastFour = message.lastFour; + } + if (lastFour !== '') { + return `card ending in ${lastFour}`; + } + return ''; +} + +function normalizeSpendRuleEffect(raw: string): 'allow' | 'block' | '' { + const v = raw.toLowerCase(); + if (v === 'allow' || v === 'block') { + return v; + } + return ''; +} + +function normalizeSpendRuleModeLabel(raw: string): string { + const v = raw.toLowerCase(); + if (v === 'allow' || v === 'block') { + return v; + } + return ''; +} + +function normalizeSpendRuleMaxAmountComparison(raw: string): 'under' | 'over' | '' { + const v = raw.toLowerCase(); + if (v === 'under' || v === 'over') { + return v; + } + return ''; +} + +function mixDescriptionFromSpendRuleMessage(message: OriginalMessagePolicyChangeLog): string { + if (typeof message?.mixDescription === 'string' && message.mixDescription !== '') { + return message.mixDescription; + } + const mixParts = message?.mixParts; + if (!Array.isArray(mixParts)) { + return ''; + } + const parts: string[] = []; + for (const part of mixParts) { + if (typeof part === 'string' && part !== '') { + parts.push(part); + } + } + const n = parts.length; + if (n === 0) { + return ''; + } + if (n === 1) { + return parts.at(0) ?? ''; + } + const last = parts.pop(); + return `${parts.join(', ')}, and ${last}`; +} + +function spendRuleFallbackAddOrUpdate(message: OriginalMessagePolicyChangeLog, isAdd: boolean): string { + const verb = isAdd ? 'added' : 'updated'; + const onCard = spendRuleCardScopeFragment(message); + if (onCard !== '') { + return `${verb} spend rule on ${onCard}`; + } + return `${verb} spend rule`; +} + +/** Mirrors Web-Expensify `Report_Action_PolicyChangeLog_SpendRuleMessage::buildAddOrUpdate` (structured path only; caller handles preformatted text). */ +function spendRuleBuildAddOrUpdate(message: OriginalMessagePolicyChangeLog, isAdd: boolean): string { + const effect = normalizeSpendRuleEffect(String(message?.effect ?? message?.ruleEffect ?? '')); + if (effect === '') { + return spendRuleFallbackAddOrUpdate(message, isAdd); + } + + const verb = effect === 'allow' ? 'allowed' : 'blocked'; + const summaryTypeRaw = message?.summaryType ?? message?.spendRuleSummaryType ?? ''; + const summaryType = typeof summaryTypeRaw === 'string' ? summaryTypeRaw : ''; + const onCard = spendRuleCardScopeFragment(message); + const onSuffix = onCard !== '' ? ` on ${onCard}` : ''; + + switch (summaryType) { + case 'merchant': { + const merchant = typeof message?.merchant === 'string' ? message.merchant : ''; + if (merchant === '') { + return spendRuleFallbackAddOrUpdate(message, isAdd); + } + return `${verb} ${merchant}${onSuffix}`; + } + case 'category': { + const category = typeof message?.category === 'string' ? message.category : ''; + if (category === '') { + return spendRuleFallbackAddOrUpdate(message, isAdd); + } + return `${verb} ${category}${onSuffix}`; + } + case 'max_amount': { + const comparison = normalizeSpendRuleMaxAmountComparison(typeof message?.maxAmountComparison === 'string' ? message.maxAmountComparison : ''); + const display = typeof message?.maxAmountDisplay === 'string' ? message.maxAmountDisplay : ''; + if (comparison === '' || display === '') { + return spendRuleFallbackAddOrUpdate(message, isAdd); + } + return `${verb} amounts ${comparison} ${display}${onSuffix}`; + } + case 'mix': { + const mix = mixDescriptionFromSpendRuleMessage(message); + if (mix === '') { + return spendRuleFallbackAddOrUpdate(message, isAdd); + } + return `${verb} ${mix.replace(/\.\s*$/, '')}.`; + } + default: + return spendRuleFallbackAddOrUpdate(message, isAdd); + } +} + +function isSpendRuleModeOnlyChange(message: OriginalMessagePolicyChangeLog): boolean { + const kind = message?.spendRuleChangeKind ?? message?.changeType ?? ''; + return typeof kind === 'string' && kind.toLowerCase() === 'mode'; +} + +function spendRuleBuildModeChange(message: OriginalMessagePolicyChangeLog): string { + const from = normalizeSpendRuleModeLabel(typeof message?.fromSpendRuleMode === 'string' ? message.fromSpendRuleMode : String(message?.fromMode ?? '')); + const to = normalizeSpendRuleModeLabel(typeof message?.toSpendRuleMode === 'string' ? message.toSpendRuleMode : String(message?.toMode ?? '')); + const onCard = spendRuleCardScopeFragment(message); + if (from !== '' && to !== '' && onCard !== '') { + return `changed spend rule from ${from} to ${to} on ${onCard}`; + } + if (from !== '' && to !== '') { + return `changed spend rule from ${from} to ${to}`; + } + return 'updated spend rule'; +} + +function spendRuleBuildRemove(message: OriginalMessagePolicyChangeLog): string { + const scope = spendRuleCardScopeFragment(message); + if (scope !== '') { + return `removed spend rule from ${scope}`; + } + return 'removed spend rule'; +} + +function getAddExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAction: OnyxEntry): string { + if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE)) { + return ''; + } + const message = getOriginalMessage(reportAction) ?? {}; + const pre = getSpendRulePreformattedText(message); + if (pre) { + return pre; + } + return spendRuleBuildAddOrUpdate(message, true); +} + +function getUpdateExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAction: OnyxEntry): string { + if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE)) { + return ''; + } + const message = getOriginalMessage(reportAction) ?? {}; + const pre = getSpendRulePreformattedText(message); + if (pre) { + return pre; + } + if (isSpendRuleModeOnlyChange(message)) { + return spendRuleBuildModeChange(message); + } + return spendRuleBuildAddOrUpdate(message, false); +} + +function getRemoveExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAction: OnyxEntry): string { + if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE)) { + return ''; + } + const message = getOriginalMessage(reportAction) ?? {}; + const pre = getSpendRulePreformattedText(message); + if (pre) { + return pre; + } + return spendRuleBuildRemove(message); +} + function getRemovedFromApprovalChainMessage(translate: LocalizedTranslate, reportAction: OnyxEntry>) { const originalMessage = getOriginalMessage(reportAction); const submittersNames = getPersonalDetailsByIDs({ @@ -4581,6 +4797,7 @@ export { getOneTransactionThreadReportAction, getOneTransactionThreadReportID, getOriginalMessage, + getAddExpensifyCardRuleMessage, getAddedApprovalRuleMessage, getDeletedApprovalRuleMessage, getUpdatedApprovalRuleMessage, @@ -4592,8 +4809,10 @@ export { getReportActionMessage, getReportActionMessageText, getReportActionText, + getRemoveExpensifyCardRuleMessage, getSortedReportActions, getSortedReportActionsForDisplay, + getUpdateExpensifyCardRuleMessage, isCardBrokenConnectionAction, getCardConnectionBrokenMessage, getTextFromHtml, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index b4068d56f90c..1b3aca2c13d0 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -42,6 +42,7 @@ import { getAddedBudgetMessage, getAddedCardFeedMessage, getAddedConnectionMessage, + getAddExpensifyCardRuleMessage, getAssignedCompanyCardMessage, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, @@ -78,6 +79,7 @@ import { getReimburserUpdateMessage, getRemovedCardFeedMessage, getRemovedConnectionMessage, + getRemoveExpensifyCardRuleMessage, getRenamedAction, getRenamedCardFeedMessage, getReportAction, @@ -109,6 +111,7 @@ import { getUpdatedSharedBudgetNotificationMessage, getUpdatedTimeEnabledMessage, getUpdatedTimeRateMessage, + getUpdateExpensifyCardRuleMessage, getUpdateRoomDescriptionMessage, getWorkspaceAttendeeTrackingUpdateMessage, getWorkspaceCategoriesUpdatedMessage, @@ -1161,6 +1164,12 @@ function getOptionData({ result.alternateText = getDeletedApprovalRuleMessage(translate, lastAction); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_APPROVER_RULE) { result.alternateText = getUpdatedApprovalRuleMessage(translate, lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE) { + result.alternateText = getAddExpensifyCardRuleMessage(translate, lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE) { + result.alternateText = getUpdateExpensifyCardRuleMessage(translate, lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE) { + result.alternateText = getRemoveExpensifyCardRuleMessage(translate, lastAction); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MANUAL_APPROVAL_THRESHOLD) { result.alternateText = getUpdatedManualApprovalThresholdMessage(translate, lastAction); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_BUDGET) { diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx index ca78eb8814d1..f5219c7df122 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx @@ -33,6 +33,7 @@ import { getAddedBudgetMessage, getAddedCardFeedMessage, getAddedConnectionMessage, + getAddExpensifyCardRuleMessage, getAssignedCompanyCardMessage, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, @@ -77,6 +78,7 @@ import { getReimburserUpdateMessage, getRemovedCardFeedMessage, getRemovedConnectionMessage, + getRemoveExpensifyCardRuleMessage, getRenamedAction, getRenamedCardFeedMessage, getReportAction, @@ -108,6 +110,7 @@ import { getUpdatedSharedBudgetNotificationMessage, getUpdatedTimeEnabledMessage, getUpdatedTimeRateMessage, + getUpdateExpensifyCardRuleMessage, getUpdateRoomDescriptionMessage, getWorkspaceAttendeeTrackingUpdateMessage, getWorkspaceCategoriesUpdatedMessage, @@ -1121,6 +1124,12 @@ const ContextMenuActions: ContextMenuAction[] = [ setClipboardMessage(getDeletedApprovalRuleMessage(translate, reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_APPROVER_RULE)) { setClipboardMessage(getUpdatedApprovalRuleMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE)) { + setClipboardMessage(getAddExpensifyCardRuleMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE)) { + setClipboardMessage(getUpdateExpensifyCardRuleMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE)) { + setClipboardMessage(getRemoveExpensifyCardRuleMessage(translate, reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MANUAL_APPROVAL_THRESHOLD)) { setClipboardMessage(getUpdatedManualApprovalThresholdMessage(translate, reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_BUDGET)) { diff --git a/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx b/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx index 63ac39058150..5d20c3f7ed78 100644 --- a/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx +++ b/src/pages/inbox/report/actionContents/PolicyChangeLogContent.tsx @@ -10,6 +10,7 @@ import { getAddedBudgetMessage, getAddedCardFeedMessage, getAddedConnectionMessage, + getAddExpensifyCardRuleMessage, getAssignedCompanyCardMessage, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, @@ -36,6 +37,7 @@ import { getReimburserUpdateMessage, getRemovedCardFeedMessage, getRemovedConnectionMessage, + getRemoveExpensifyCardRuleMessage, getRenamedCardFeedMessage, getRequireCompanyCardsEnabledMessage, getSetAutoJoinMessage, @@ -60,6 +62,7 @@ import { getUpdatedSharedBudgetNotificationMessage, getUpdatedTimeEnabledMessage, getUpdatedTimeRateMessage, + getUpdateExpensifyCardRuleMessage, getWorkspaceAttendeeTrackingUpdateMessage, getWorkspaceCategoriesUpdatedMessage, getWorkspaceCategoryUpdateMessage, @@ -163,6 +166,9 @@ const POLICY_CHANGE_LOG_RESOLVERS: Record = { [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_APPROVER_RULE]: (translate, action) => getAddedApprovalRuleMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_APPROVER_RULE]: (translate, action) => getDeletedApprovalRuleMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_APPROVER_RULE]: (translate, action) => getUpdatedApprovalRuleMessage(translate, action), + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE]: (translate, action) => getAddExpensifyCardRuleMessage(translate, action), + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE]: (translate, action) => getUpdateExpensifyCardRuleMessage(translate, action), + [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE]: (translate, action) => getRemoveExpensifyCardRuleMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_INTEGRATION]: (translate, action) => getAddedConnectionMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_INTEGRATION]: (translate, action) => getRemovedConnectionMessage(translate, action), [CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CARD_FEED]: (translate, action) => getAddedCardFeedMessage(translate, action), diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 022d6307c330..2e3be2cd2fba 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -764,6 +764,72 @@ type OriginalMessagePolicyChangeLog = { /** Whether the user joined the workspace via joining link */ didJoinPolicy?: boolean; + + /** Preformatted spend rule changelog line from the server */ + changeLogText?: string; + + /** Alternate preformatted spend rule changelog key (`text`) */ + text?: string; + + /** Alternate preformatted spend rule changelog key (`displayMessage`) */ + displayMessage?: string; + + /** Spend rule summary discriminator (`merchant`, `category`, `max_amount`, or `mix`) */ + summaryType?: string; + + /** Alias for summaryType */ + spendRuleSummaryType?: string; + + /** Spend rule effect (`block` or `allow`) */ + effect?: string; + + /** Alias for effect */ + ruleEffect?: string; + + /** Merchant name for merchant summary */ + merchant?: string; + + /** Category label for category summary */ + category?: string; + + /** Max amount comparison (`under` or `over`) */ + maxAmountComparison?: string; + + /** Human-readable max amount (e.g. "$1,000") */ + maxAmountDisplay?: string; + + /** Pre-built mix summary description */ + mixDescription?: string; + + /** Mix summary fragments joined with Oxford "and" */ + mixParts?: string[]; + + /** Single-card label (e.g. "Todd's Card") */ + cardName?: string; + + /** Number of cards when greater than 1 */ + cardCount?: number | string; + + /** Update kind (`mode` for block/allow toggle-only updates) */ + spendRuleChangeKind?: string; + + /** Alias for spendRuleChangeKind */ + changeType?: string; + + /** Previous spend rule mode for mode-only updates */ + fromSpendRuleMode?: string; + + /** Alias for fromSpendRuleMode */ + fromMode?: string; + + /** New spend rule mode for mode-only updates */ + toSpendRuleMode?: string; + + /** Alias for toSpendRuleMode */ + toMode?: string; + + /** Card last four when cardName is absent (alias) */ + lastFour?: string; }; /** Model of `join policy` report action */ From 9793421337880c1c3942bff7f3b2fbcf36c5b6f7 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 10:47:46 -0300 Subject: [PATCH 02/17] update add message --- src/languages/en.ts | 36 ++++++++++++ src/languages/es.ts | 36 ++++++++++++ src/libs/ReportActionsUtils.ts | 97 +++++++++++++++++++++++++++++-- src/types/onyx/OriginalMessage.ts | 79 +++++++------------------ 4 files changed, 185 insertions(+), 63 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index d1550f9a5098..d27c0a5ce050 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7273,6 +7273,42 @@ const translations = { updatedCardFeedLiability: (feedName: string, enabled: boolean) => `${enabled ? 'enabled' : 'disabled'} cardholders to delete card transactions for card feed "${feedName}"`, updatedCardFeedStatementPeriod: (feedName: string, newValue?: string, previousValue?: string) => `changed card feed "${feedName}" statement period end day${newValue ? ` to "${newValue}"` : ''}${previousValue ? ` (previously "${previousValue}")` : ''}`, + expensifyCardRule: { + actionVerb: { + block: 'blocked', + allow: 'allowed', + }, + amountOperator: { + over: 'over', + under: 'under', + }, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `amounts ${operator} ${amount}`, + theCard: 'the card', + namedCard: ({name}: {name: string}) => `'${name}'`, + multipleCards: ({count}: {count: number}) => `${count} cards`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} and ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')}, and ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '}on ${cards}`; + } + return text; + }, + }, preventSelfApproval: (oldValue: string, newValue: string) => `updated "Prevent self-approval" to "${newValue === 'true' ? 'Enabled' : 'Disabled'}" (previously "${oldValue === 'true' ? 'Enabled' : 'Disabled'}")`, updateMonthlyOffset: (oldValue: string, newValue: string) => { diff --git a/src/languages/es.ts b/src/languages/es.ts index 4d165fcfc90c..bb2976dedadb 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7126,6 +7126,42 @@ ${amount} para ${merchant} - ${date}`, `${enabled ? 'habilitó' : 'deshabilitó'} que los titulares de tarjetas eliminen transacciones de la fuente de tarjetas "${feedName}"`, updatedCardFeedStatementPeriod: (feedName: string, newValue?: string, previousValue?: string) => `cambió el día de cierre del período de estado de cuenta de la fuente de tarjetas "${feedName}"${newValue ? ` a "${newValue}"` : ''}${previousValue ? ` (previamente "${previousValue}")` : ''}`, + expensifyCardRule: { + actionVerb: { + block: 'bloqueó', + allow: 'permitió', + }, + amountOperator: { + over: 'mayores a', + under: 'menores a', + }, + amountFilter: ({operator, amount}) => `montos ${operator} ${amount}`, + theCard: 'la tarjeta', + namedCard: ({name}) => `'${name}'`, + multipleCards: ({count}) => `${count} tarjetas`, + joinFilters: ({items}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} y ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')} y ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '}en ${cards}`; + } + return text; + }, + }, preventSelfApproval: (oldValue, newValue) => `actualizó "Evitar la autoaprobación" a "${newValue === 'true' ? 'Habilitada' : 'Deshabilitada'}" (previamente "${oldValue === 'true' ? 'Habilitada' : 'Deshabilitada'}")`, setReceiptRequiredAmount: (newValue) => `estableció el importe requerido del recibo en "${newValue}"`, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 7512059cd2e4..40c4eae4b2c0 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -15,6 +15,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 {isSpendRuleCategory} from '@src/types/form/SpendRuleForm'; import type { Card, CompanyCardFeed, @@ -3903,7 +3904,6 @@ function getUpdatedApprovalRuleMessage(translate: LocalizedTranslate, reportActi return getReportActionText(reportAction); } -/** Mirrors Web-Expensify `Report_Action_PolicyChangeLog_SpendRuleMessage::preformattedText` */ function getSpendRulePreformattedText(message: OriginalMessagePolicyChangeLog): string | undefined { for (const key of ['changeLogText', 'text', 'displayMessage'] as const) { const value = message?.[key]; @@ -4005,7 +4005,6 @@ function spendRuleFallbackAddOrUpdate(message: OriginalMessagePolicyChangeLog, i return `${verb} spend rule`; } -/** Mirrors Web-Expensify `Report_Action_PolicyChangeLog_SpendRuleMessage::buildAddOrUpdate` (structured path only; caller handles preformatted text). */ function spendRuleBuildAddOrUpdate(message: OriginalMessagePolicyChangeLog, isAdd: boolean): string { const effect = normalizeSpendRuleEffect(String(message?.effect ?? message?.ruleEffect ?? '')); if (effect === '') { @@ -4079,7 +4078,67 @@ function spendRuleBuildRemove(message: OriginalMessagePolicyChangeLog): string { return 'removed spend rule'; } -function getAddExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAction: OnyxEntry): string { +function spendRuleActionVerb(translate: LocalizedTranslate, action: string): string { + if (action === CONST.SPEND_RULES.ACTION.BLOCK) { + return translate('workspaceActions.expensifyCardRule.actionVerb.block'); + } + if (action === CONST.SPEND_RULES.ACTION.ALLOW) { + return translate('workspaceActions.expensifyCardRule.actionVerb.allow'); + } + return ''; +} + +function spendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: string): string { + if (operator === 'lte') { + return translate('workspaceActions.expensifyCardRule.amountOperator.under'); + } + if (operator === 'gte') { + return translate('workspaceActions.expensifyCardRule.amountOperator.over'); + } + return ''; +} + +function spendRuleFormatAmountFilter(translate: LocalizedTranslate, amount: {operator?: unknown; value?: unknown}, currency: string): string { + const operator = typeof amount?.operator === 'string' ? amount.operator : ''; + const operatorWord = spendRuleAmountOperatorWord(translate, operator); + if (operatorWord === '') { + return ''; + } + const valueArray: unknown[] = Array.isArray(amount?.value) ? (amount.value as unknown[]) : []; + const firstValue = valueArray.at(0); + let cents = 0; + if (typeof firstValue === 'string' && firstValue !== '' && Number.isFinite(Number(firstValue))) { + cents = Number.parseInt(firstValue, 10); + } else if (typeof firstValue === 'number' && Number.isFinite(firstValue)) { + cents = firstValue; + } + return translate('workspaceActions.expensifyCardRule.amountFilter', {operator: operatorWord, amount: convertToShortDisplayString(cents, currency)}); +} + +function spendRuleCardsSummary(translate: LocalizedTranslate, cards: ReadonlyArray<{displayName?: string}> | undefined): string { + if (!cards || cards.length === 0) { + return translate('workspaceActions.expensifyCardRule.theCard'); + } + if (cards.length === 1) { + const displayName = cards.at(0)?.displayName ?? ''; + return displayName !== '' ? translate('workspaceActions.expensifyCardRule.namedCard', {name: displayName}) : translate('workspaceActions.expensifyCardRule.theCard'); + } + return translate('workspaceActions.expensifyCardRule.multipleCards', {count: cards.length}); +} + +function spendRuleJoinFilters(translate: LocalizedTranslate, items: readonly string[]): string { + const filtered = items.filter((value) => typeof value === 'string' && value !== ''); + return translate('workspaceActions.expensifyCardRule.joinFilters', {items: filtered}); +} + +function spendRuleCategoryDisplayName(translate: LocalizedTranslate, slug: string): string { + if (isSpendRuleCategory(slug)) { + return translate(`workspace.rules.spendRules.categoryOptions.${slug}`); + } + return slug; +} + +function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE)) { return ''; } @@ -4088,7 +4147,37 @@ function getAddExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAc if (pre) { return pre; } - return spendRuleBuildAddOrUpdate(message, true); + + const action = typeof message?.action === 'string' ? message.action : ''; + const currency = typeof message?.currency === 'string' && message.currency !== '' ? message.currency : CONST.CURRENCY.USD; + const merchants = (message?.merchants ?? []).filter((value) => typeof value === 'string' && value !== ''); + const categories = (message?.categories ?? []).filter((value) => typeof value === 'string' && value !== ''); + const amounts = message?.amounts ?? []; + const cards = message?.cards ?? []; + + const items: string[] = []; + for (const merchant of merchants) { + items.push(merchant); + } + for (const category of categories) { + items.push(spendRuleCategoryDisplayName(translate, category)); + } + for (const amount of amounts) { + const formatted = spendRuleFormatAmountFilter(translate, amount, currency); + if (formatted !== '') { + items.push(formatted); + } + } + + const verb = spendRuleActionVerb(translate, action); + const filtersDesc = spendRuleJoinFilters(translate, items); + const cardsSummary = spendRuleCardsSummary(translate, cards); + + if (verb === '' && filtersDesc === '' && cardsSummary === '') { + return spendRuleFallbackAddOrUpdate(message, true); + } + + return translate('workspaceActions.expensifyCardRule.addRule', {verb, filters: filtersDesc, cards: cardsSummary}); } function getUpdateExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAction: OnyxEntry): string { diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 7cff880513b0..6c5d456aec45 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -765,71 +765,32 @@ type OriginalMessagePolicyChangeLog = { /** Whether the user joined the workspace via joining link */ didJoinPolicy?: boolean; - /** Preformatted spend rule changelog line from the server */ - changeLogText?: string; + /** Spend rule action (`block` or `allow`) sent by the new structured changelog payload */ + action?: string; - /** Alternate preformatted spend rule changelog key (`text`) */ - text?: string; + /** Merchants included in a spend rule */ + merchants?: string[]; - /** Alternate preformatted spend rule changelog key (`displayMessage`) */ - displayMessage?: string; + /** Categories (slugs) included in a spend rule */ + categories?: string[]; - /** Spend rule summary discriminator (`merchant`, `category`, `max_amount`, or `mix`) */ - summaryType?: string; + /** Max-amount filters in a spend rule */ + amounts?: Array<{ + /** Operator (`gte` for "over", `lte` for "under") */ + operator: string; - /** Alias for summaryType */ - spendRuleSummaryType?: string; - - /** Spend rule effect (`block` or `allow`) */ - effect?: string; - - /** Alias for effect */ - ruleEffect?: string; - - /** Merchant name for merchant summary */ - merchant?: string; - - /** Category label for category summary */ - category?: string; - - /** Max amount comparison (`under` or `over`) */ - maxAmountComparison?: string; - - /** Human-readable max amount (e.g. "$1,000") */ - maxAmountDisplay?: string; - - /** Pre-built mix summary description */ - mixDescription?: string; - - /** Mix summary fragments joined with Oxford "and" */ - mixParts?: string[]; - - /** Single-card label (e.g. "Todd's Card") */ - cardName?: string; - - /** Number of cards when greater than 1 */ - cardCount?: number | string; - - /** Update kind (`mode` for block/allow toggle-only updates) */ - spendRuleChangeKind?: string; - - /** Alias for spendRuleChangeKind */ - changeType?: string; - - /** Previous spend rule mode for mode-only updates */ - fromSpendRuleMode?: string; - - /** Alias for fromSpendRuleMode */ - fromMode?: string; - - /** New spend rule mode for mode-only updates */ - toSpendRuleMode?: string; + /** Amount value as cents serialized to a string array (`['100000']`) */ + value: string[]; + }>; - /** Alias for toSpendRuleMode */ - toMode?: string; + /** Cards a spend rule is scoped to */ + cards?: Array<{ + /** Card identifier */ + cardID: number | string; - /** Card last four when cardName is absent (alias) */ - lastFour?: string; + /** Display name shown when the rule covers a single card */ + displayName?: string; + }>; }; /** Model of `join policy` report action */ From 63a150137413ca920b3dfc6e0700d8a09276f1e9 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 11:00:36 -0300 Subject: [PATCH 03/17] add remove case --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/ReportActionsUtils.ts | 188 +-------------------------------- 3 files changed, 7 insertions(+), 183 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index d27c0a5ce050..f641475bc8b9 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7308,6 +7308,7 @@ const translations = { } return text; }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `removed spend rule from ${cards}` : 'removed spend rule'), }, preventSelfApproval: (oldValue: string, newValue: string) => `updated "Prevent self-approval" to "${newValue === 'true' ? 'Enabled' : 'Disabled'}" (previously "${oldValue === 'true' ? 'Enabled' : 'Disabled'}")`, diff --git a/src/languages/es.ts b/src/languages/es.ts index bb2976dedadb..c0fd14d06aa3 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7161,6 +7161,7 @@ ${amount} para ${merchant} - ${date}`, } return text; }, + removeRule: ({cards}) => (cards !== '' ? `eliminó la regla de gasto de ${cards}` : 'eliminó la regla de gasto'), }, preventSelfApproval: (oldValue, newValue) => `actualizó "Evitar la autoaprobación" a "${newValue === 'true' ? 'Habilitada' : 'Deshabilitada'}" (previamente "${oldValue === 'true' ? 'Habilitada' : 'Deshabilitada'}")`, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 40c4eae4b2c0..078f5bb92ef9 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3914,170 +3914,6 @@ function getSpendRulePreformattedText(message: OriginalMessagePolicyChangeLog): return undefined; } -function cardCountFromSpendRuleMessage(message: OriginalMessagePolicyChangeLog): number { - const raw = message?.cardCount; - if (typeof raw === 'number' && Number.isFinite(raw)) { - return raw; - } - if (typeof raw === 'string' && raw !== '' && /^\d+$/.test(raw)) { - return Number.parseInt(raw, 10); - } - return 0; -} - -function spendRuleCardScopeFragment(message: OriginalMessagePolicyChangeLog): string { - const cardCount = cardCountFromSpendRuleMessage(message); - const cardName = typeof message?.cardName === 'string' ? message.cardName : ''; - if (cardCount > 1) { - return `${cardCount} cards`; - } - if (cardName !== '') { - return cardName; - } - if (cardCount === 1) { - return '1 card'; - } - let lastFour = typeof message?.cardLastFour === 'string' ? message.cardLastFour : ''; - if (lastFour === '' && typeof message?.lastFour === 'string') { - lastFour = message.lastFour; - } - if (lastFour !== '') { - return `card ending in ${lastFour}`; - } - return ''; -} - -function normalizeSpendRuleEffect(raw: string): 'allow' | 'block' | '' { - const v = raw.toLowerCase(); - if (v === 'allow' || v === 'block') { - return v; - } - return ''; -} - -function normalizeSpendRuleModeLabel(raw: string): string { - const v = raw.toLowerCase(); - if (v === 'allow' || v === 'block') { - return v; - } - return ''; -} - -function normalizeSpendRuleMaxAmountComparison(raw: string): 'under' | 'over' | '' { - const v = raw.toLowerCase(); - if (v === 'under' || v === 'over') { - return v; - } - return ''; -} - -function mixDescriptionFromSpendRuleMessage(message: OriginalMessagePolicyChangeLog): string { - if (typeof message?.mixDescription === 'string' && message.mixDescription !== '') { - return message.mixDescription; - } - const mixParts = message?.mixParts; - if (!Array.isArray(mixParts)) { - return ''; - } - const parts: string[] = []; - for (const part of mixParts) { - if (typeof part === 'string' && part !== '') { - parts.push(part); - } - } - const n = parts.length; - if (n === 0) { - return ''; - } - if (n === 1) { - return parts.at(0) ?? ''; - } - const last = parts.pop(); - return `${parts.join(', ')}, and ${last}`; -} - -function spendRuleFallbackAddOrUpdate(message: OriginalMessagePolicyChangeLog, isAdd: boolean): string { - const verb = isAdd ? 'added' : 'updated'; - const onCard = spendRuleCardScopeFragment(message); - if (onCard !== '') { - return `${verb} spend rule on ${onCard}`; - } - return `${verb} spend rule`; -} - -function spendRuleBuildAddOrUpdate(message: OriginalMessagePolicyChangeLog, isAdd: boolean): string { - const effect = normalizeSpendRuleEffect(String(message?.effect ?? message?.ruleEffect ?? '')); - if (effect === '') { - return spendRuleFallbackAddOrUpdate(message, isAdd); - } - - const verb = effect === 'allow' ? 'allowed' : 'blocked'; - const summaryTypeRaw = message?.summaryType ?? message?.spendRuleSummaryType ?? ''; - const summaryType = typeof summaryTypeRaw === 'string' ? summaryTypeRaw : ''; - const onCard = spendRuleCardScopeFragment(message); - const onSuffix = onCard !== '' ? ` on ${onCard}` : ''; - - switch (summaryType) { - case 'merchant': { - const merchant = typeof message?.merchant === 'string' ? message.merchant : ''; - if (merchant === '') { - return spendRuleFallbackAddOrUpdate(message, isAdd); - } - return `${verb} ${merchant}${onSuffix}`; - } - case 'category': { - const category = typeof message?.category === 'string' ? message.category : ''; - if (category === '') { - return spendRuleFallbackAddOrUpdate(message, isAdd); - } - return `${verb} ${category}${onSuffix}`; - } - case 'max_amount': { - const comparison = normalizeSpendRuleMaxAmountComparison(typeof message?.maxAmountComparison === 'string' ? message.maxAmountComparison : ''); - const display = typeof message?.maxAmountDisplay === 'string' ? message.maxAmountDisplay : ''; - if (comparison === '' || display === '') { - return spendRuleFallbackAddOrUpdate(message, isAdd); - } - return `${verb} amounts ${comparison} ${display}${onSuffix}`; - } - case 'mix': { - const mix = mixDescriptionFromSpendRuleMessage(message); - if (mix === '') { - return spendRuleFallbackAddOrUpdate(message, isAdd); - } - return `${verb} ${mix.replace(/\.\s*$/, '')}.`; - } - default: - return spendRuleFallbackAddOrUpdate(message, isAdd); - } -} - -function isSpendRuleModeOnlyChange(message: OriginalMessagePolicyChangeLog): boolean { - const kind = message?.spendRuleChangeKind ?? message?.changeType ?? ''; - return typeof kind === 'string' && kind.toLowerCase() === 'mode'; -} - -function spendRuleBuildModeChange(message: OriginalMessagePolicyChangeLog): string { - const from = normalizeSpendRuleModeLabel(typeof message?.fromSpendRuleMode === 'string' ? message.fromSpendRuleMode : String(message?.fromMode ?? '')); - const to = normalizeSpendRuleModeLabel(typeof message?.toSpendRuleMode === 'string' ? message.toSpendRuleMode : String(message?.toMode ?? '')); - const onCard = spendRuleCardScopeFragment(message); - if (from !== '' && to !== '' && onCard !== '') { - return `changed spend rule from ${from} to ${to} on ${onCard}`; - } - if (from !== '' && to !== '') { - return `changed spend rule from ${from} to ${to}`; - } - return 'updated spend rule'; -} - -function spendRuleBuildRemove(message: OriginalMessagePolicyChangeLog): string { - const scope = spendRuleCardScopeFragment(message); - if (scope !== '') { - return `removed spend rule from ${scope}`; - } - return 'removed spend rule'; -} - function spendRuleActionVerb(translate: LocalizedTranslate, action: string): string { if (action === CONST.SPEND_RULES.ACTION.BLOCK) { return translate('workspaceActions.expensifyCardRule.actionVerb.block'); @@ -4174,28 +4010,13 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct const cardsSummary = spendRuleCardsSummary(translate, cards); if (verb === '' && filtersDesc === '' && cardsSummary === '') { - return spendRuleFallbackAddOrUpdate(message, true); + return getReportActionText(reportAction); } return translate('workspaceActions.expensifyCardRule.addRule', {verb, filters: filtersDesc, cards: cardsSummary}); } -function getUpdateExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAction: OnyxEntry): string { - if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE)) { - return ''; - } - const message = getOriginalMessage(reportAction) ?? {}; - const pre = getSpendRulePreformattedText(message); - if (pre) { - return pre; - } - if (isSpendRuleModeOnlyChange(message)) { - return spendRuleBuildModeChange(message); - } - return spendRuleBuildAddOrUpdate(message, false); -} - -function getRemoveExpensifyCardRuleMessage(_translate: LocalizedTranslate, reportAction: OnyxEntry): string { +function getRemoveExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE)) { return ''; } @@ -4204,7 +4025,9 @@ function getRemoveExpensifyCardRuleMessage(_translate: LocalizedTranslate, repor if (pre) { return pre; } - return spendRuleBuildRemove(message); + const cards = message?.cards ?? []; + const cardsSummary = spendRuleCardsSummary(translate, cards); + return translate('workspaceActions.expensifyCardRule.removeRule', {cards: cardsSummary}); } function getRemovedFromApprovalChainMessage(translate: LocalizedTranslate, reportAction: OnyxEntry>) { @@ -4898,7 +4721,6 @@ export { getRemoveExpensifyCardRuleMessage, getSortedReportActions, getSortedReportActionsForDisplay, - getUpdateExpensifyCardRuleMessage, isCardBrokenConnectionAction, getCardConnectionBrokenMessage, getTextFromHtml, From fb62a4d8fab0d8388edd71441ba65eafa1922d11 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 11:13:00 -0300 Subject: [PATCH 04/17] add update case --- src/languages/en.ts | 29 +++ src/languages/es.ts | 29 +++ src/libs/ReportActionsUtils.ts | 333 +++++++++++++++++++++++++++++++++ 3 files changed, 391 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index f641475bc8b9..4d8e81915b28 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7309,6 +7309,35 @@ const translations = { return text; }, removeRule: ({cards}: {cards: string}) => (cards !== '' ? `removed spend rule from ${cards}` : 'removed spend rule'), + restrictionVerb: { + block: 'block', + allow: 'only allow', + }, + update: { + modeChange: ({fromMode, toMode, cards}: {fromMode: string; toMode: string; cards: string}) => + cards !== '' ? `changed spend rule from ${fromMode} to ${toMode} on ${cards}` : `changed spend rule from ${fromMode} to ${toMode}`, + appliedToAdditionalCards: ({count}: {count: number}) => `applied spend rule to ${count} additional cards`, + phraseVerb: { + added: 'added', + removed: 'removed', + changed: 'changed', + set: 'set', + applied: 'applied', + }, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} merchant '${value}'` : `merchant '${value}'`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} merchant from '${oldValue}' to '${newValue}'` : `merchant from '${oldValue}' to '${newValue}'`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} spend category '${value}'` : `spend category '${value}'`), + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} spend category from '${oldValue}' to '${newValue}'` : `spend category from '${oldValue}' to '${newValue}'`, + bodyMaxAmount: 'max amount', + bodyMaxAmountSet: ({value}: {value: string}) => `max amount to ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `max amount from ${oldValue} to ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `spend rule to ${count} additional cards`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `spend rule from ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} on ${cards}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} from ${cards}` : content), + }, }, preventSelfApproval: (oldValue: string, newValue: string) => `updated "Prevent self-approval" to "${newValue === 'true' ? 'Enabled' : 'Disabled'}" (previously "${oldValue === 'true' ? 'Enabled' : 'Disabled'}")`, diff --git a/src/languages/es.ts b/src/languages/es.ts index c0fd14d06aa3..850321339687 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7162,6 +7162,35 @@ ${amount} para ${merchant} - ${date}`, return text; }, removeRule: ({cards}) => (cards !== '' ? `eliminó la regla de gasto de ${cards}` : 'eliminó la regla de gasto'), + restrictionVerb: { + block: 'bloquear', + allow: 'solo permitir', + }, + update: { + modeChange: ({fromMode, toMode, cards}) => + cards !== '' ? `cambió la regla de gasto de ${fromMode} a ${toMode} en ${cards}` : `cambió la regla de gasto de ${fromMode} a ${toMode}`, + appliedToAdditionalCards: ({count}) => `aplicó la regla de gasto a ${count} tarjetas adicionales`, + phraseVerb: { + added: 'agregó', + removed: 'eliminó', + changed: 'cambió', + set: 'estableció', + applied: 'aplicó', + }, + bodyMerchant: ({adjective, value}) => (adjective !== '' ? `comerciante ${adjective} '${value}'` : `comerciante '${value}'`), + bodyMerchantChange: ({adjective, oldValue, newValue}) => + adjective !== '' ? `comerciante ${adjective} de '${oldValue}' a '${newValue}'` : `comerciante de '${oldValue}' a '${newValue}'`, + bodySpendCategory: ({adjective, value}) => (adjective !== '' ? `categoría de gasto ${adjective} '${value}'` : `categoría de gasto '${value}'`), + bodySpendCategoryChange: ({adjective, oldValue, newValue}) => + adjective !== '' ? `categoría de gasto ${adjective} de '${oldValue}' a '${newValue}'` : `categoría de gasto de '${oldValue}' a '${newValue}'`, + bodyMaxAmount: 'monto máximo', + bodyMaxAmountSet: ({value}) => `monto máximo en ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}) => `monto máximo de ${oldValue} a ${newValue}`, + bodyAppliedToAdditionalCards: ({count}) => `la regla de gasto a ${count} tarjetas adicionales`, + bodyRemovedFromCards: ({cards}) => `la regla de gasto de ${cards}`, + composeOnCards: ({content, cards}) => (cards !== '' ? `${content} en ${cards}` : content), + composeFromCards: ({content, cards}) => (cards !== '' ? `${content} de ${cards}` : content), + }, }, preventSelfApproval: (oldValue, newValue) => `actualizó "Evitar la autoaprobación" a "${newValue === 'true' ? 'Habilitada' : 'Deshabilitada'}" (previamente "${oldValue === 'true' ? 'Habilitada' : 'Deshabilitada'}")`, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 078f5bb92ef9..77c03cbeff1b 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3974,6 +3974,179 @@ function spendRuleCategoryDisplayName(translate: LocalizedTranslate, slug: strin return slug; } +function spendRuleRestrictionVerb(translate: LocalizedTranslate, action: string): string { + if (action === CONST.SPEND_RULES.ACTION.BLOCK) { + return translate('workspaceActions.expensifyCardRule.restrictionVerb.block'); + } + if (action === CONST.SPEND_RULES.ACTION.ALLOW) { + return translate('workspaceActions.expensifyCardRule.restrictionVerb.allow'); + } + return action; +} + +function spendRuleAmountToCents(value: unknown): number { + const valueArray: unknown[] = Array.isArray(value) ? (value as unknown[]) : []; + const firstValue = valueArray.at(0); + if (typeof firstValue === 'string' && firstValue !== '' && Number.isFinite(Number(firstValue))) { + return Number.parseInt(firstValue, 10); + } + if (typeof firstValue === 'number' && Number.isFinite(firstValue)) { + return firstValue; + } + return 0; +} + +function spendRuleFormatAmountValue(amount: {value?: unknown}, currency: string): string { + return convertToShortDisplayString(spendRuleAmountToCents(amount?.value), currency); +} + +function spendRuleAmountKey(amount: {operator?: unknown; value?: unknown}): string { + const operator = typeof amount?.operator === 'string' ? amount.operator : ''; + return `${operator}:${spendRuleAmountToCents(amount?.value)}`; +} + +type SpendRuleStringDiff = {added: string[]; removed: string[]}; + +function spendRuleStringDiff(oldValues: readonly string[], newValues: readonly string[]): SpendRuleStringDiff { + const oldSet = Array.from(new Set(oldValues)); + const newSet = Array.from(new Set(newValues)); + const added = newSet.filter((value) => !oldSet.includes(value)).sort(); + const removed = oldSet.filter((value) => !newSet.includes(value)).sort(); + return {added, removed}; +} + +type SpendRuleAmount = {operator?: unknown; value?: unknown}; +type SpendRuleAmountDiff = {added: SpendRuleAmount[]; removed: SpendRuleAmount[]}; + +function spendRuleAmountDiff(oldAmounts: readonly SpendRuleAmount[], newAmounts: readonly SpendRuleAmount[]): SpendRuleAmountDiff { + const oldByKey = new Map(); + for (const amount of oldAmounts) { + oldByKey.set(spendRuleAmountKey(amount), amount); + } + const newByKey = new Map(); + for (const amount of newAmounts) { + newByKey.set(spendRuleAmountKey(amount), amount); + } + const added: SpendRuleAmount[] = []; + for (const [key, amount] of newByKey) { + if (!oldByKey.has(key)) { + added.push(amount); + } + } + const removed: SpendRuleAmount[] = []; + for (const [key, amount] of oldByKey) { + if (!newByKey.has(key)) { + removed.push(amount); + } + } + return {added, removed}; +} + +type SpendRuleCard = {cardID?: number | string; displayName?: string}; +type SpendRuleCardDiff = {added: SpendRuleCard[]; removed: SpendRuleCard[]}; + +function spendRuleCardID(card: SpendRuleCard): number | undefined { + const raw = card?.cardID; + if (typeof raw === 'number' && Number.isFinite(raw)) { + return raw; + } + if (typeof raw === 'string' && /^\d+$/.test(raw)) { + return Number.parseInt(raw, 10); + } + return undefined; +} + +function spendRuleCardDiff(oldCards: readonly SpendRuleCard[], newCards: readonly SpendRuleCard[]): SpendRuleCardDiff { + const oldByID = new Map(); + for (const card of oldCards) { + const id = spendRuleCardID(card); + if (id !== undefined) { + oldByID.set(id, card); + } + } + const newByID = new Map(); + for (const card of newCards) { + const id = spendRuleCardID(card); + if (id !== undefined) { + newByID.set(id, card); + } + } + const added: SpendRuleCard[] = []; + for (const [id, card] of newByID) { + if (!oldByID.has(id)) { + added.push(card); + } + } + const removed: SpendRuleCard[] = []; + for (const [id, card] of oldByID) { + if (!newByID.has(id)) { + removed.push(card); + } + } + return {added, removed}; +} + +type SpendRulePhraseVerb = 'added' | 'removed' | 'changed' | 'set' | 'applied'; +type SpendRulePhraseAdjective = '' | typeof CONST.SPEND_RULES.ACTION.BLOCK | typeof CONST.SPEND_RULES.ACTION.ALLOW; + +type SpendRulePhrase = { + verb: SpendRulePhraseVerb; + adjective: SpendRulePhraseAdjective; + bodyWithAdjective: string; + bodyWithoutAdjective: string; +}; + +function spendRulePhraseVerbWord(translate: LocalizedTranslate, verb: SpendRulePhraseVerb): string { + return translate(`workspaceActions.expensifyCardRule.update.phraseVerb.${verb}`); +} + +function spendRuleAdjectiveWord(translate: LocalizedTranslate, adjective: SpendRulePhraseAdjective): string { + if (adjective === CONST.SPEND_RULES.ACTION.BLOCK) { + return translate('workspaceActions.expensifyCardRule.actionVerb.block'); + } + if (adjective === CONST.SPEND_RULES.ACTION.ALLOW) { + return translate('workspaceActions.expensifyCardRule.actionVerb.allow'); + } + return ''; +} + +function joinSpendRulePhrases(translate: LocalizedTranslate, phrases: readonly SpendRulePhrase[]): string { + if (phrases.length === 0) { + return ''; + } + if (phrases.length === 1) { + const phrase = phrases.at(0); + if (!phrase) { + return ''; + } + return `${spendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`; + } + + const firstVerb = phrases.at(0)?.verb; + const allSameVerb = firstVerb !== undefined && phrases.every((phrase) => phrase.verb === firstVerb); + + if (!allSameVerb) { + const parts = phrases.map((phrase) => `${spendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`); + return spendRuleJoinFilters(translate, parts); + } + + const firstPhrase = phrases.at(0); + if (!firstPhrase) { + return ''; + } + const firstAdjective = firstPhrase.adjective; + const parts: string[] = [`${spendRulePhraseVerbWord(translate, firstPhrase.verb)} ${firstPhrase.bodyWithAdjective}`]; + for (let i = 1; i < phrases.length; i++) { + const phrase = phrases.at(i); + if (!phrase) { + continue; + } + const useOwnAdjective = phrase.adjective !== '' && phrase.adjective !== firstAdjective; + parts.push(useOwnAdjective ? phrase.bodyWithAdjective : phrase.bodyWithoutAdjective); + } + return spendRuleJoinFilters(translate, parts); +} + function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EXPENSIFY_CARD_RULE)) { return ''; @@ -4016,6 +4189,165 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct return translate('workspaceActions.expensifyCardRule.addRule', {verb, filters: filtersDesc, cards: cardsSummary}); } +function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { + if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EXPENSIFY_CARD_RULE)) { + return ''; + } + const message = getOriginalMessage(reportAction) ?? {}; + const pre = getSpendRulePreformattedText(message); + if (pre) { + return pre; + } + + const oldAction = typeof message?.oldAction === 'string' ? message.oldAction : ''; + const newAction = typeof message?.action === 'string' ? message.action : ''; + const actionChanged = oldAction !== '' && oldAction !== newAction; + const currency = typeof message?.currency === 'string' && message.currency !== '' ? message.currency : CONST.CURRENCY.USD; + + const oldMerchants = (message?.oldMerchants ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); + const newMerchants = (message?.merchants ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); + const oldCategories = (message?.oldCategories ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); + const newCategories = (message?.categories ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); + const oldAmounts: SpendRuleAmount[] = (message?.oldAmounts ?? []).filter((amount): amount is SpendRuleAmount => typeof amount === 'object' && amount !== null); + const newAmounts: SpendRuleAmount[] = (message?.amounts ?? []).filter((amount): amount is SpendRuleAmount => typeof amount === 'object' && amount !== null); + const oldCards: SpendRuleCard[] = (message?.oldCards ?? []).filter((card): card is SpendRuleCard => typeof card === 'object' && card !== null); + const newCards: SpendRuleCard[] = (message?.cards ?? []).filter((card): card is SpendRuleCard => typeof card === 'object' && card !== null); + + const merchantDiff = spendRuleStringDiff(oldMerchants, newMerchants); + const categoryDiff = spendRuleStringDiff(oldCategories, newCategories); + const amountDiff = spendRuleAmountDiff(oldAmounts, newAmounts); + const cardDiff = spendRuleCardDiff(oldCards, newCards); + + const merchantsChanged = merchantDiff.added.length > 0 || merchantDiff.removed.length > 0; + const categoriesChanged = categoryDiff.added.length > 0 || categoryDiff.removed.length > 0; + const amountsChanged = amountDiff.added.length > 0 || amountDiff.removed.length > 0; + const cardsChanged = cardDiff.added.length > 0 || cardDiff.removed.length > 0; + const filtersAndCardsUnchanged = !merchantsChanged && !categoriesChanged && !amountsChanged && !cardsChanged; + + const newCardsSummary = spendRuleCardsSummary(translate, newCards); + + if (actionChanged && filtersAndCardsUnchanged) { + return translate('workspaceActions.expensifyCardRule.update.modeChange', { + fromMode: spendRuleRestrictionVerb(translate, oldAction), + toMode: spendRuleRestrictionVerb(translate, newAction), + cards: newCardsSummary, + }); + } + + if (cardsChanged && !merchantsChanged && !categoriesChanged && !amountsChanged && !actionChanged) { + if (cardDiff.added.length > 0 && cardDiff.removed.length === 0) { + return translate('workspaceActions.expensifyCardRule.update.appliedToAdditionalCards', {count: cardDiff.added.length}); + } + if (cardDiff.added.length === 0 && cardDiff.removed.length > 0) { + return translate('workspaceActions.expensifyCardRule.removeRule', {cards: spendRuleCardsSummary(translate, cardDiff.removed)}); + } + } + + const adjective: SpendRulePhraseAdjective = newAction === CONST.SPEND_RULES.ACTION.BLOCK || newAction === CONST.SPEND_RULES.ACTION.ALLOW ? newAction : ''; + const adjectiveWord = spendRuleAdjectiveWord(translate, adjective); + const phrases: SpendRulePhrase[] = []; + + if (merchantDiff.added.length === 1 && merchantDiff.removed.length === 1) { + const oldValue = merchantDiff.removed.at(0) ?? ''; + const newValue = merchantDiff.added.at(0) ?? ''; + phrases.push({ + verb: 'changed', + adjective, + bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchantChange', {adjective: adjectiveWord, oldValue, newValue}), + bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchantChange', {adjective: '', oldValue, newValue}), + }); + } else { + for (const merchant of merchantDiff.added) { + phrases.push({ + verb: 'added', + adjective, + bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchant', {adjective: adjectiveWord, value: merchant}), + bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchant', {adjective: '', value: merchant}), + }); + } + for (const merchant of merchantDiff.removed) { + phrases.push({ + verb: 'removed', + adjective, + bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchant', {adjective: adjectiveWord, value: merchant}), + bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodyMerchant', {adjective: '', value: merchant}), + }); + } + } + + if (categoryDiff.added.length === 1 && categoryDiff.removed.length === 1) { + const oldValue = spendRuleCategoryDisplayName(translate, categoryDiff.removed.at(0) ?? ''); + const newValue = spendRuleCategoryDisplayName(translate, categoryDiff.added.at(0) ?? ''); + phrases.push({ + verb: 'changed', + adjective, + bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategoryChange', {adjective: adjectiveWord, oldValue, newValue}), + bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategoryChange', {adjective: '', oldValue, newValue}), + }); + } else { + for (const category of categoryDiff.added) { + const value = spendRuleCategoryDisplayName(translate, category); + phrases.push({ + verb: 'added', + adjective, + bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', {adjective: adjectiveWord, value}), + bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', {adjective: '', value}), + }); + } + for (const category of categoryDiff.removed) { + const value = spendRuleCategoryDisplayName(translate, category); + phrases.push({ + verb: 'removed', + adjective, + bodyWithAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', {adjective: adjectiveWord, value}), + bodyWithoutAdjective: translate('workspaceActions.expensifyCardRule.update.bodySpendCategory', {adjective: '', value}), + }); + } + } + + if (amountDiff.added.length === 1 && amountDiff.removed.length === 1) { + const oldValue = spendRuleFormatAmountValue(amountDiff.removed.at(0) ?? {}, currency); + const newValue = spendRuleFormatAmountValue(amountDiff.added.at(0) ?? {}, currency); + const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountChange', {oldValue, newValue}); + phrases.push({verb: 'changed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + } else { + for (const amount of amountDiff.added) { + const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountSet', {value: spendRuleFormatAmountValue(amount, currency)}); + phrases.push({verb: 'set', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + } + for (let i = 0; i < amountDiff.removed.length; i++) { + const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmount'); + phrases.push({verb: 'removed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + } + } + + if (cardDiff.added.length > 0) { + const body = translate('workspaceActions.expensifyCardRule.update.bodyAppliedToAdditionalCards', {count: cardDiff.added.length}); + phrases.push({verb: 'applied', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + } + if (cardDiff.removed.length > 0) { + const body = translate('workspaceActions.expensifyCardRule.update.bodyRemovedFromCards', {cards: spendRuleCardsSummary(translate, cardDiff.removed)}); + phrases.push({verb: 'removed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + } + + if (phrases.length === 0) { + return getAddExpensifyCardRuleMessage(translate, reportAction); + } + + const joined = joinSpendRulePhrases(translate, phrases); + + if (cardsChanged) { + return joined; + } + + const onlyRemovedPhrase = phrases.length === 1 && phrases.at(0)?.verb === 'removed'; + if (onlyRemovedPhrase) { + return translate('workspaceActions.expensifyCardRule.update.composeFromCards', {content: joined, cards: newCardsSummary}); + } + + return translate('workspaceActions.expensifyCardRule.update.composeOnCards', {content: joined, cards: newCardsSummary}); +} + function getRemoveExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { if (!isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_EXPENSIFY_CARD_RULE)) { return ''; @@ -4707,6 +5039,7 @@ export { getOneTransactionThreadReportID, getOriginalMessage, getAddExpensifyCardRuleMessage, + getUpdateExpensifyCardRuleMessage, getAddedApprovalRuleMessage, getDeletedApprovalRuleMessage, getUpdatedApprovalRuleMessage, From 8278d8fb38d0802528adb50b69a8dc10cee288ee Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 11:26:27 -0300 Subject: [PATCH 05/17] add types --- src/libs/ReportActionsUtils.ts | 33 +++++++++++-------------------- src/types/onyx/OriginalMessage.ts | 27 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 77c03cbeff1b..ecbe5e595b61 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -4100,16 +4100,6 @@ function spendRulePhraseVerbWord(translate: LocalizedTranslate, verb: SpendRuleP return translate(`workspaceActions.expensifyCardRule.update.phraseVerb.${verb}`); } -function spendRuleAdjectiveWord(translate: LocalizedTranslate, adjective: SpendRulePhraseAdjective): string { - if (adjective === CONST.SPEND_RULES.ACTION.BLOCK) { - return translate('workspaceActions.expensifyCardRule.actionVerb.block'); - } - if (adjective === CONST.SPEND_RULES.ACTION.ALLOW) { - return translate('workspaceActions.expensifyCardRule.actionVerb.allow'); - } - return ''; -} - function joinSpendRulePhrases(translate: LocalizedTranslate, phrases: readonly SpendRulePhrase[]): string { if (phrases.length === 0) { return ''; @@ -4204,14 +4194,14 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report const actionChanged = oldAction !== '' && oldAction !== newAction; const currency = typeof message?.currency === 'string' && message.currency !== '' ? message.currency : CONST.CURRENCY.USD; - const oldMerchants = (message?.oldMerchants ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); - const newMerchants = (message?.merchants ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); - const oldCategories = (message?.oldCategories ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); - const newCategories = (message?.categories ?? []).filter((value): value is string => typeof value === 'string' && value !== ''); - const oldAmounts: SpendRuleAmount[] = (message?.oldAmounts ?? []).filter((amount): amount is SpendRuleAmount => typeof amount === 'object' && amount !== null); - const newAmounts: SpendRuleAmount[] = (message?.amounts ?? []).filter((amount): amount is SpendRuleAmount => typeof amount === 'object' && amount !== null); - const oldCards: SpendRuleCard[] = (message?.oldCards ?? []).filter((card): card is SpendRuleCard => typeof card === 'object' && card !== null); - const newCards: SpendRuleCard[] = (message?.cards ?? []).filter((card): card is SpendRuleCard => typeof card === 'object' && card !== null); + const oldMerchants = (message?.oldMerchants ?? []).filter((value) => value !== ''); + const newMerchants = (message?.merchants ?? []).filter((value) => value !== ''); + const oldCategories = (message?.oldCategories ?? []).filter((value) => value !== ''); + const newCategories = (message?.categories ?? []).filter((value) => value !== ''); + const oldAmounts = message?.oldAmounts ?? []; + const newAmounts = message?.amounts ?? []; + const oldCards = message?.oldCards ?? []; + const newCards = message?.cards ?? []; const merchantDiff = spendRuleStringDiff(oldMerchants, newMerchants); const categoryDiff = spendRuleStringDiff(oldCategories, newCategories); @@ -4244,7 +4234,7 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report } const adjective: SpendRulePhraseAdjective = newAction === CONST.SPEND_RULES.ACTION.BLOCK || newAction === CONST.SPEND_RULES.ACTION.ALLOW ? newAction : ''; - const adjectiveWord = spendRuleAdjectiveWord(translate, adjective); + const adjectiveWord = spendRuleActionVerb(translate, adjective); const phrases: SpendRulePhrase[] = []; if (merchantDiff.added.length === 1 && merchantDiff.removed.length === 1) { @@ -4315,9 +4305,10 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountSet', {value: spendRuleFormatAmountValue(amount, currency)}); phrases.push({verb: 'set', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); } - for (let i = 0; i < amountDiff.removed.length; i++) { + if (amountDiff.removed.length > 0) { const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmount'); - phrases.push({verb: 'removed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); + const removedPhrase: SpendRulePhrase = {verb: 'removed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}; + phrases.push(...Array.from({length: amountDiff.removed.length}).fill(removedPhrase)); } } diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 6c5d456aec45..9eccb2e23a9d 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -768,12 +768,21 @@ type OriginalMessagePolicyChangeLog = { /** Spend rule action (`block` or `allow`) sent by the new structured changelog payload */ action?: string; + /** Previous spend rule action when the rule's restriction type changed in an update */ + oldAction?: string; + /** Merchants included in a spend rule */ merchants?: string[]; + /** Previous list of merchants when a spend rule was updated */ + oldMerchants?: string[]; + /** Categories (slugs) included in a spend rule */ categories?: string[]; + /** Previous list of categories when a spend rule was updated */ + oldCategories?: string[]; + /** Max-amount filters in a spend rule */ amounts?: Array<{ /** Operator (`gte` for "over", `lte` for "under") */ @@ -783,6 +792,15 @@ type OriginalMessagePolicyChangeLog = { value: string[]; }>; + /** Previous list of max-amount filters when a spend rule was updated */ + oldAmounts?: Array<{ + /** Operator (`gte` for "over", `lte` for "under") */ + operator: string; + + /** Amount value as cents serialized to a string array (`['100000']`) */ + value: string[]; + }>; + /** Cards a spend rule is scoped to */ cards?: Array<{ /** Card identifier */ @@ -791,6 +809,15 @@ type OriginalMessagePolicyChangeLog = { /** Display name shown when the rule covers a single card */ displayName?: string; }>; + + /** Previous list of cards when a spend rule's card scope was updated */ + oldCards?: Array<{ + /** Card identifier */ + cardID: number | string; + + /** Display name shown when the rule covers a single card */ + displayName?: string; + }>; }; /** Model of `join policy` report action */ From 031af8fa3fa171e7873aa2dbeb4e6cc170f0b50e Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 13:02:07 -0300 Subject: [PATCH 06/17] rm pre --- src/libs/ReportActionsUtils.ts | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ecbe5e595b61..1b2dbfe5bb97 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3904,16 +3904,6 @@ function getUpdatedApprovalRuleMessage(translate: LocalizedTranslate, reportActi return getReportActionText(reportAction); } -function getSpendRulePreformattedText(message: OriginalMessagePolicyChangeLog): string | undefined { - for (const key of ['changeLogText', 'text', 'displayMessage'] as const) { - const value = message?.[key]; - if (typeof value === 'string' && value !== '') { - return value; - } - } - return undefined; -} - function spendRuleActionVerb(translate: LocalizedTranslate, action: string): string { if (action === CONST.SPEND_RULES.ACTION.BLOCK) { return translate('workspaceActions.expensifyCardRule.actionVerb.block'); @@ -4142,11 +4132,6 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct return ''; } const message = getOriginalMessage(reportAction) ?? {}; - const pre = getSpendRulePreformattedText(message); - if (pre) { - return pre; - } - const action = typeof message?.action === 'string' ? message.action : ''; const currency = typeof message?.currency === 'string' && message.currency !== '' ? message.currency : CONST.CURRENCY.USD; const merchants = (message?.merchants ?? []).filter((value) => typeof value === 'string' && value !== ''); @@ -4184,11 +4169,6 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report return ''; } const message = getOriginalMessage(reportAction) ?? {}; - const pre = getSpendRulePreformattedText(message); - if (pre) { - return pre; - } - const oldAction = typeof message?.oldAction === 'string' ? message.oldAction : ''; const newAction = typeof message?.action === 'string' ? message.action : ''; const actionChanged = oldAction !== '' && oldAction !== newAction; @@ -4344,10 +4324,6 @@ function getRemoveExpensifyCardRuleMessage(translate: LocalizedTranslate, report return ''; } const message = getOriginalMessage(reportAction) ?? {}; - const pre = getSpendRulePreformattedText(message); - if (pre) { - return pre; - } const cards = message?.cards ?? []; const cardsSummary = spendRuleCardsSummary(translate, cards); return translate('workspaceActions.expensifyCardRule.removeRule', {cards: cardsSummary}); From e6d23fdcc9257778343a48e2bc4f72cc85d40232 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 14:10:05 -0300 Subject: [PATCH 07/17] rename category function --- src/libs/ReportActionsUtils.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 1b2dbfe5bb97..bd798629464f 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3957,11 +3957,11 @@ function spendRuleJoinFilters(translate: LocalizedTranslate, items: readonly str return translate('workspaceActions.expensifyCardRule.joinFilters', {items: filtered}); } -function spendRuleCategoryDisplayName(translate: LocalizedTranslate, slug: string): string { - if (isSpendRuleCategory(slug)) { - return translate(`workspace.rules.spendRules.categoryOptions.${slug}`); +function getSpendRuleCategoryDisplayName(translate: LocalizedTranslate, category: string): string { + if (isSpendRuleCategory(category)) { + return translate(`workspace.rules.spendRules.categoryOptions.${category}`); } - return slug; + return category; } function spendRuleRestrictionVerb(translate: LocalizedTranslate, action: string): string { @@ -4132,19 +4132,19 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct return ''; } const message = getOriginalMessage(reportAction) ?? {}; - const action = typeof message?.action === 'string' ? message.action : ''; - const currency = typeof message?.currency === 'string' && message.currency !== '' ? message.currency : CONST.CURRENCY.USD; - const merchants = (message?.merchants ?? []).filter((value) => typeof value === 'string' && value !== ''); - const categories = (message?.categories ?? []).filter((value) => typeof value === 'string' && value !== ''); - const amounts = message?.amounts ?? []; - const cards = message?.cards ?? []; + const action = message.action ?? CONST.SPEND_RULES.ACTION.ALLOW; + const currency = message.currency ?? CONST.CURRENCY.USD; + const merchants = message.merchants ?? []; + const categories = message.categories ?? []; + const amounts = message.amounts ?? []; + const cards = message.cards ?? []; const items: string[] = []; for (const merchant of merchants) { items.push(merchant); } for (const category of categories) { - items.push(spendRuleCategoryDisplayName(translate, category)); + items.push(getSpendRuleCategoryDisplayName(translate, category)); } for (const amount of amounts) { const formatted = spendRuleFormatAmountFilter(translate, amount, currency); @@ -4246,8 +4246,8 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report } if (categoryDiff.added.length === 1 && categoryDiff.removed.length === 1) { - const oldValue = spendRuleCategoryDisplayName(translate, categoryDiff.removed.at(0) ?? ''); - const newValue = spendRuleCategoryDisplayName(translate, categoryDiff.added.at(0) ?? ''); + const oldValue = getSpendRuleCategoryDisplayName(translate, categoryDiff.removed.at(0) ?? ''); + const newValue = getSpendRuleCategoryDisplayName(translate, categoryDiff.added.at(0) ?? ''); phrases.push({ verb: 'changed', adjective, @@ -4256,7 +4256,7 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report }); } else { for (const category of categoryDiff.added) { - const value = spendRuleCategoryDisplayName(translate, category); + const value = getSpendRuleCategoryDisplayName(translate, category); phrases.push({ verb: 'added', adjective, @@ -4265,7 +4265,7 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report }); } for (const category of categoryDiff.removed) { - const value = spendRuleCategoryDisplayName(translate, category); + const value = getSpendRuleCategoryDisplayName(translate, category); phrases.push({ verb: 'removed', adjective, From 418297f1d63f567a02ef49ca6b698788e36b480f Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 14:15:58 -0300 Subject: [PATCH 08/17] update remove case --- src/libs/ReportActionsUtils.ts | 60 +++++++++++++++------------------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index bd798629464f..1b5b577d8c21 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3904,7 +3904,7 @@ function getUpdatedApprovalRuleMessage(translate: LocalizedTranslate, reportActi return getReportActionText(reportAction); } -function spendRuleActionVerb(translate: LocalizedTranslate, action: string): string { +function getSpendRuleActionVerb(translate: LocalizedTranslate, action: string): string { if (action === CONST.SPEND_RULES.ACTION.BLOCK) { return translate('workspaceActions.expensifyCardRule.actionVerb.block'); } @@ -3915,33 +3915,25 @@ function spendRuleActionVerb(translate: LocalizedTranslate, action: string): str } function spendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: string): string { - if (operator === 'lte') { + if (operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN) { return translate('workspaceActions.expensifyCardRule.amountOperator.under'); } - if (operator === 'gte') { + if (operator === CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN) { return translate('workspaceActions.expensifyCardRule.amountOperator.over'); } return ''; } -function spendRuleFormatAmountFilter(translate: LocalizedTranslate, amount: {operator?: unknown; value?: unknown}, currency: string): string { - const operator = typeof amount?.operator === 'string' ? amount.operator : ''; - const operatorWord = spendRuleAmountOperatorWord(translate, operator); - if (operatorWord === '') { +function getSpendRuleAmountString(translate: LocalizedTranslate, amount: {operator: string; value: string[]}, currency: string): string { + const operatorWord = spendRuleAmountOperatorWord(translate, amount.operator); + const firstValue = amount.value.at(0); + if (firstValue === undefined) { return ''; } - const valueArray: unknown[] = Array.isArray(amount?.value) ? (amount.value as unknown[]) : []; - const firstValue = valueArray.at(0); - let cents = 0; - if (typeof firstValue === 'string' && firstValue !== '' && Number.isFinite(Number(firstValue))) { - cents = Number.parseInt(firstValue, 10); - } else if (typeof firstValue === 'number' && Number.isFinite(firstValue)) { - cents = firstValue; - } - return translate('workspaceActions.expensifyCardRule.amountFilter', {operator: operatorWord, amount: convertToShortDisplayString(cents, currency)}); + return translate('workspaceActions.expensifyCardRule.amountFilter', {operator: operatorWord, amount: convertAmountToDisplayString(Number(firstValue), currency)}); } -function spendRuleCardsSummary(translate: LocalizedTranslate, cards: ReadonlyArray<{displayName?: string}> | undefined): string { +function getSpendRuleCardsSummary(translate: LocalizedTranslate, cards: ReadonlyArray<{displayName?: string}> | undefined): string { if (!cards || cards.length === 0) { return translate('workspaceActions.expensifyCardRule.theCard'); } @@ -3952,7 +3944,7 @@ function spendRuleCardsSummary(translate: LocalizedTranslate, cards: ReadonlyArr return translate('workspaceActions.expensifyCardRule.multipleCards', {count: cards.length}); } -function spendRuleJoinFilters(translate: LocalizedTranslate, items: readonly string[]): string { +function getSpendRuleJoinFilters(translate: LocalizedTranslate, items: readonly string[]): string { const filtered = items.filter((value) => typeof value === 'string' && value !== ''); return translate('workspaceActions.expensifyCardRule.joinFilters', {items: filtered}); } @@ -4107,7 +4099,7 @@ function joinSpendRulePhrases(translate: LocalizedTranslate, phrases: readonly S if (!allSameVerb) { const parts = phrases.map((phrase) => `${spendRulePhraseVerbWord(translate, phrase.verb)} ${phrase.bodyWithAdjective}`); - return spendRuleJoinFilters(translate, parts); + return getSpendRuleJoinFilters(translate, parts); } const firstPhrase = phrases.at(0); @@ -4124,7 +4116,7 @@ function joinSpendRulePhrases(translate: LocalizedTranslate, phrases: readonly S const useOwnAdjective = phrase.adjective !== '' && phrase.adjective !== firstAdjective; parts.push(useOwnAdjective ? phrase.bodyWithAdjective : phrase.bodyWithoutAdjective); } - return spendRuleJoinFilters(translate, parts); + return getSpendRuleJoinFilters(translate, parts); } function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { @@ -4147,21 +4139,21 @@ function getAddExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAct items.push(getSpendRuleCategoryDisplayName(translate, category)); } for (const amount of amounts) { - const formatted = spendRuleFormatAmountFilter(translate, amount, currency); - if (formatted !== '') { - items.push(formatted); + const formattedAmount = getSpendRuleAmountString(translate, amount, currency); + if (formattedAmount !== '') { + items.push(formattedAmount); } } - const verb = spendRuleActionVerb(translate, action); - const filtersDesc = spendRuleJoinFilters(translate, items); - const cardsSummary = spendRuleCardsSummary(translate, cards); + const verb = getSpendRuleActionVerb(translate, action); + const filters = getSpendRuleJoinFilters(translate, items); + const cardsSummary = getSpendRuleCardsSummary(translate, cards); - if (verb === '' && filtersDesc === '' && cardsSummary === '') { + if (verb === '' && filters === '' && cardsSummary === '') { return getReportActionText(reportAction); } - return translate('workspaceActions.expensifyCardRule.addRule', {verb, filters: filtersDesc, cards: cardsSummary}); + return translate('workspaceActions.expensifyCardRule.addRule', {verb, filters, cards: cardsSummary}); } function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, reportAction: OnyxEntry): string { @@ -4194,7 +4186,7 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report const cardsChanged = cardDiff.added.length > 0 || cardDiff.removed.length > 0; const filtersAndCardsUnchanged = !merchantsChanged && !categoriesChanged && !amountsChanged && !cardsChanged; - const newCardsSummary = spendRuleCardsSummary(translate, newCards); + const newCardsSummary = getSpendRuleCardsSummary(translate, newCards); if (actionChanged && filtersAndCardsUnchanged) { return translate('workspaceActions.expensifyCardRule.update.modeChange', { @@ -4209,12 +4201,12 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report return translate('workspaceActions.expensifyCardRule.update.appliedToAdditionalCards', {count: cardDiff.added.length}); } if (cardDiff.added.length === 0 && cardDiff.removed.length > 0) { - return translate('workspaceActions.expensifyCardRule.removeRule', {cards: spendRuleCardsSummary(translate, cardDiff.removed)}); + return translate('workspaceActions.expensifyCardRule.removeRule', {cards: getSpendRuleCardsSummary(translate, cardDiff.removed)}); } } const adjective: SpendRulePhraseAdjective = newAction === CONST.SPEND_RULES.ACTION.BLOCK || newAction === CONST.SPEND_RULES.ACTION.ALLOW ? newAction : ''; - const adjectiveWord = spendRuleActionVerb(translate, adjective); + const adjectiveWord = getSpendRuleActionVerb(translate, adjective); const phrases: SpendRulePhrase[] = []; if (merchantDiff.added.length === 1 && merchantDiff.removed.length === 1) { @@ -4297,7 +4289,7 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report phrases.push({verb: 'applied', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); } if (cardDiff.removed.length > 0) { - const body = translate('workspaceActions.expensifyCardRule.update.bodyRemovedFromCards', {cards: spendRuleCardsSummary(translate, cardDiff.removed)}); + const body = translate('workspaceActions.expensifyCardRule.update.bodyRemovedFromCards', {cards: getSpendRuleCardsSummary(translate, cardDiff.removed)}); phrases.push({verb: 'removed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); } @@ -4324,8 +4316,8 @@ function getRemoveExpensifyCardRuleMessage(translate: LocalizedTranslate, report return ''; } const message = getOriginalMessage(reportAction) ?? {}; - const cards = message?.cards ?? []; - const cardsSummary = spendRuleCardsSummary(translate, cards); + const cards = message.cards ?? []; + const cardsSummary = getSpendRuleCardsSummary(translate, cards); return translate('workspaceActions.expensifyCardRule.removeRule', {cards: cardsSummary}); } From a74f5fa4e8424a430b4cbbc889454cb39c586cbc Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 14:39:18 -0300 Subject: [PATCH 09/17] update vars --- src/languages/en.ts | 4 +- src/languages/es.ts | 4 +- src/libs/ReportActionsUtils.ts | 82 ++++++++++++++-------------------- 3 files changed, 38 insertions(+), 52 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 4d8e81915b28..e9a8f2b67a1c 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7314,8 +7314,8 @@ const translations = { allow: 'only allow', }, update: { - modeChange: ({fromMode, toMode, cards}: {fromMode: string; toMode: string; cards: string}) => - cards !== '' ? `changed spend rule from ${fromMode} to ${toMode} on ${cards}` : `changed spend rule from ${fromMode} to ${toMode}`, + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `changed spend rule from ${fromAction} to ${toAction} on ${cards}` : `changed spend rule from ${fromAction} to ${toAction}`, appliedToAdditionalCards: ({count}: {count: number}) => `applied spend rule to ${count} additional cards`, phraseVerb: { added: 'added', diff --git a/src/languages/es.ts b/src/languages/es.ts index 850321339687..f47a7330e81f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7167,8 +7167,8 @@ ${amount} para ${merchant} - ${date}`, allow: 'solo permitir', }, update: { - modeChange: ({fromMode, toMode, cards}) => - cards !== '' ? `cambió la regla de gasto de ${fromMode} a ${toMode} en ${cards}` : `cambió la regla de gasto de ${fromMode} a ${toMode}`, + modeChange: ({fromAction, toAction, cards}) => + cards !== '' ? `cambió la regla de gasto de ${fromAction} a ${toAction} en ${cards}` : `cambió la regla de gasto de ${fromAction} a ${toAction}`, appliedToAdditionalCards: ({count}) => `aplicó la regla de gasto a ${count} tarjetas adicionales`, phraseVerb: { added: 'agregó', diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 1b5b577d8c21..9907dda296f4 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3956,7 +3956,7 @@ function getSpendRuleCategoryDisplayName(translate: LocalizedTranslate, category return category; } -function spendRuleRestrictionVerb(translate: LocalizedTranslate, action: string): string { +function getSpendRuleRestrictionVerb(translate: LocalizedTranslate, action: string): string { if (action === CONST.SPEND_RULES.ACTION.BLOCK) { return translate('workspaceActions.expensifyCardRule.restrictionVerb.block'); } @@ -3982,14 +3982,9 @@ function spendRuleFormatAmountValue(amount: {value?: unknown}, currency: string) return convertToShortDisplayString(spendRuleAmountToCents(amount?.value), currency); } -function spendRuleAmountKey(amount: {operator?: unknown; value?: unknown}): string { - const operator = typeof amount?.operator === 'string' ? amount.operator : ''; - return `${operator}:${spendRuleAmountToCents(amount?.value)}`; -} - type SpendRuleStringDiff = {added: string[]; removed: string[]}; -function spendRuleStringDiff(oldValues: readonly string[], newValues: readonly string[]): SpendRuleStringDiff { +function computeSpendRuleStringDiff(oldValues: string[], newValues: string[]): SpendRuleStringDiff { const oldSet = Array.from(new Set(oldValues)); const newSet = Array.from(new Set(newValues)); const added = newSet.filter((value) => !oldSet.includes(value)).sort(); @@ -4000,28 +3995,19 @@ function spendRuleStringDiff(oldValues: readonly string[], newValues: readonly s type SpendRuleAmount = {operator?: unknown; value?: unknown}; type SpendRuleAmountDiff = {added: SpendRuleAmount[]; removed: SpendRuleAmount[]}; -function spendRuleAmountDiff(oldAmounts: readonly SpendRuleAmount[], newAmounts: readonly SpendRuleAmount[]): SpendRuleAmountDiff { - const oldByKey = new Map(); - for (const amount of oldAmounts) { - oldByKey.set(spendRuleAmountKey(amount), amount); - } - const newByKey = new Map(); - for (const amount of newAmounts) { - newByKey.set(spendRuleAmountKey(amount), amount); - } - const added: SpendRuleAmount[] = []; - for (const [key, amount] of newByKey) { - if (!oldByKey.has(key)) { - added.push(amount); - } - } - const removed: SpendRuleAmount[] = []; - for (const [key, amount] of oldByKey) { - if (!newByKey.has(key)) { - removed.push(amount); - } +function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: SpendRuleAmount[]): SpendRuleAmountDiff { + const oldAmount = oldAmounts.at(0); + const newAmount = newAmounts.at(0); + const sameAmount = + oldAmount?.operator === newAmount?.operator && + spendRuleAmountToCents(oldAmount?.value) === spendRuleAmountToCents(newAmount?.value); + if (sameAmount) { + return {added: [], removed: []}; } - return {added, removed}; + return { + added: newAmount ? [newAmount] : [], + removed: oldAmount ? [oldAmount] : [], + }; } type SpendRuleCard = {cardID?: number | string; displayName?: string}; @@ -4038,7 +4024,7 @@ function spendRuleCardID(card: SpendRuleCard): number | undefined { return undefined; } -function spendRuleCardDiff(oldCards: readonly SpendRuleCard[], newCards: readonly SpendRuleCard[]): SpendRuleCardDiff { +function computeSpendRuleCardDiff(oldCards: SpendRuleCard[], newCards: SpendRuleCard[]): SpendRuleCardDiff { const oldByID = new Map(); for (const card of oldCards) { const id = spendRuleCardID(card); @@ -4161,24 +4147,24 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report return ''; } const message = getOriginalMessage(reportAction) ?? {}; - const oldAction = typeof message?.oldAction === 'string' ? message.oldAction : ''; - const newAction = typeof message?.action === 'string' ? message.action : ''; + const oldAction = message.oldAction ?? CONST.SPEND_RULES.ACTION.ALLOW; + const newAction = message.action ?? CONST.SPEND_RULES.ACTION.ALLOW; const actionChanged = oldAction !== '' && oldAction !== newAction; - const currency = typeof message?.currency === 'string' && message.currency !== '' ? message.currency : CONST.CURRENCY.USD; - - const oldMerchants = (message?.oldMerchants ?? []).filter((value) => value !== ''); - const newMerchants = (message?.merchants ?? []).filter((value) => value !== ''); - const oldCategories = (message?.oldCategories ?? []).filter((value) => value !== ''); - const newCategories = (message?.categories ?? []).filter((value) => value !== ''); - const oldAmounts = message?.oldAmounts ?? []; - const newAmounts = message?.amounts ?? []; - const oldCards = message?.oldCards ?? []; - const newCards = message?.cards ?? []; - - const merchantDiff = spendRuleStringDiff(oldMerchants, newMerchants); - const categoryDiff = spendRuleStringDiff(oldCategories, newCategories); - const amountDiff = spendRuleAmountDiff(oldAmounts, newAmounts); - const cardDiff = spendRuleCardDiff(oldCards, newCards); + const currency = message.currency ?? CONST.CURRENCY.USD; + + const oldMerchants = message.oldMerchants ?? []; + const newMerchants = message.merchants ?? []; + const oldCategories = message.oldCategories ?? []; + const newCategories = message.categories ?? []; + const oldAmounts = message.oldAmounts ?? []; + const newAmounts = message.amounts ?? []; + const oldCards = message.oldCards ?? []; + const newCards = message.cards ?? []; + + const merchantDiff = computeSpendRuleStringDiff(oldMerchants, newMerchants); + const categoryDiff = computeSpendRuleStringDiff(oldCategories, newCategories); + const amountDiff = computeSpendRuleAmountDiff(oldAmounts, newAmounts); + const cardDiff = computeSpendRuleCardDiff(oldCards, newCards); const merchantsChanged = merchantDiff.added.length > 0 || merchantDiff.removed.length > 0; const categoriesChanged = categoryDiff.added.length > 0 || categoryDiff.removed.length > 0; @@ -4190,8 +4176,8 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report if (actionChanged && filtersAndCardsUnchanged) { return translate('workspaceActions.expensifyCardRule.update.modeChange', { - fromMode: spendRuleRestrictionVerb(translate, oldAction), - toMode: spendRuleRestrictionVerb(translate, newAction), + fromAction: getSpendRuleRestrictionVerb(translate, oldAction), + toAction: getSpendRuleRestrictionVerb(translate, newAction), cards: newCardsSummary, }); } From 52bf786daf82cf05e04d5c29f55b61328591fbd0 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 14:39:50 -0300 Subject: [PATCH 10/17] fix prettier --- src/libs/ReportActionsUtils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 9907dda296f4..6e06f20eec27 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3998,9 +3998,7 @@ type SpendRuleAmountDiff = {added: SpendRuleAmount[]; removed: SpendRuleAmount[] function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: SpendRuleAmount[]): SpendRuleAmountDiff { const oldAmount = oldAmounts.at(0); const newAmount = newAmounts.at(0); - const sameAmount = - oldAmount?.operator === newAmount?.operator && - spendRuleAmountToCents(oldAmount?.value) === spendRuleAmountToCents(newAmount?.value); + const sameAmount = oldAmount?.operator === newAmount?.operator && spendRuleAmountToCents(oldAmount?.value) === spendRuleAmountToCents(newAmount?.value); if (sameAmount) { return {added: [], removed: []}; } From 66d00a55230c5b25716a468d58a51b894fff11a2 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 14:47:56 -0300 Subject: [PATCH 11/17] rm unused import --- src/libs/ReportActionsUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 6e06f20eec27..ba8d373731a1 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -34,7 +34,6 @@ import type { OriginalMessageChangeLog, OriginalMessageExportIntegration, OriginalMessageMarkedReimbursed, - OriginalMessagePolicyChangeLog, OriginalMessageReimbursed, OriginalMessageUnreportedTransaction, PolicyBudgetFrequency, From 666d76a4c03ffc682fd2fafad604525984893682 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 15:15:34 -0300 Subject: [PATCH 12/17] fix operator --- src/libs/ReportActionsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ba8d373731a1..ff5389d4b94f 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3914,7 +3914,7 @@ function getSpendRuleActionVerb(translate: LocalizedTranslate, action: string): } function spendRuleAmountOperatorWord(translate: LocalizedTranslate, operator: string): string { - if (operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN) { + if (operator === CONST.SEARCH.SYNTAX_OPERATORS.LOWER_THAN_OR_EQUAL_TO) { return translate('workspaceActions.expensifyCardRule.amountOperator.under'); } if (operator === CONST.SEARCH.SYNTAX_OPERATORS.GREATER_THAN) { From 56929197f878055219b78a3d9dfd3f7323a5b180 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 15:24:35 -0300 Subject: [PATCH 13/17] fix amounts --- src/libs/ReportActionsUtils.ts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index ff5389d4b94f..80979487293a 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3965,20 +3965,16 @@ function getSpendRuleRestrictionVerb(translate: LocalizedTranslate, action: stri return action; } -function spendRuleAmountToCents(value: unknown): number { - const valueArray: unknown[] = Array.isArray(value) ? (value as unknown[]) : []; - const firstValue = valueArray.at(0); - if (typeof firstValue === 'string' && firstValue !== '' && Number.isFinite(Number(firstValue))) { - return Number.parseInt(firstValue, 10); +function spendRuleAmountToCents(value: string[]): number { + const firstValue = value.at(0) ?? ''; + if (firstValue === '' || !Number.isFinite(Number(firstValue))) { + return 0; } - if (typeof firstValue === 'number' && Number.isFinite(firstValue)) { - return firstValue; - } - return 0; + return Number.parseInt(firstValue, 10) * 100; } -function spendRuleFormatAmountValue(amount: {value?: unknown}, currency: string): string { - return convertToShortDisplayString(spendRuleAmountToCents(amount?.value), currency); +function spendRuleFormatAmountValue(amount: {value: string[]}, currency: string): string { + return convertAmountToDisplayString(spendRuleAmountToCents(amount.value), currency); } type SpendRuleStringDiff = {added: string[]; removed: string[]}; @@ -3991,13 +3987,16 @@ function computeSpendRuleStringDiff(oldValues: string[], newValues: string[]): S return {added, removed}; } -type SpendRuleAmount = {operator?: unknown; value?: unknown}; +type SpendRuleAmount = {operator: string; value: string[]}; type SpendRuleAmountDiff = {added: SpendRuleAmount[]; removed: SpendRuleAmount[]}; function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: SpendRuleAmount[]): SpendRuleAmountDiff { const oldAmount = oldAmounts.at(0); const newAmount = newAmounts.at(0); - const sameAmount = oldAmount?.operator === newAmount?.operator && spendRuleAmountToCents(oldAmount?.value) === spendRuleAmountToCents(newAmount?.value); + if (!oldAmount || !newAmount) { + return {added: [], removed: []}; + } + const sameAmount = oldAmount.operator === newAmount.operator && spendRuleAmountToCents(oldAmount.value) === spendRuleAmountToCents(newAmount.value); if (sameAmount) { return {added: [], removed: []}; } From 8c02dd99391bf132f9d924fb23392fee111339aa Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 15:44:32 -0300 Subject: [PATCH 14/17] fix same amount case --- src/libs/ReportActionsUtils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 80979487293a..46ebd309b0de 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3965,7 +3965,7 @@ function getSpendRuleRestrictionVerb(translate: LocalizedTranslate, action: stri return action; } -function spendRuleAmountToCents(value: string[]): number { +function formatSpendRuleAmountToCents(value: string[]): number { const firstValue = value.at(0) ?? ''; if (firstValue === '' || !Number.isFinite(Number(firstValue))) { return 0; @@ -3974,7 +3974,7 @@ function spendRuleAmountToCents(value: string[]): number { } function spendRuleFormatAmountValue(amount: {value: string[]}, currency: string): string { - return convertAmountToDisplayString(spendRuleAmountToCents(amount.value), currency); + return convertAmountToDisplayString(formatSpendRuleAmountToCents(amount.value), currency); } type SpendRuleStringDiff = {added: string[]; removed: string[]}; @@ -3996,13 +3996,13 @@ function computeSpendRuleAmountDiff(oldAmounts: SpendRuleAmount[], newAmounts: S if (!oldAmount || !newAmount) { return {added: [], removed: []}; } - const sameAmount = oldAmount.operator === newAmount.operator && spendRuleAmountToCents(oldAmount.value) === spendRuleAmountToCents(newAmount.value); + const sameAmount = formatSpendRuleAmountToCents(oldAmount.value) === formatSpendRuleAmountToCents(newAmount.value); if (sameAmount) { return {added: [], removed: []}; } return { - added: newAmount ? [newAmount] : [], - removed: oldAmount ? [oldAmount] : [], + added: [newAmount], + removed: [oldAmount], }; } From ec0ff21f5a3cdf3c1de49c52557bc0decfe8eeed Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 16:30:20 -0300 Subject: [PATCH 15/17] add copy --- src/languages/de.ts | 51 ++++++++++++++++++++++++++++++++++++++++ src/languages/fr.ts | 51 ++++++++++++++++++++++++++++++++++++++++ src/languages/it.ts | 51 ++++++++++++++++++++++++++++++++++++++++ src/languages/ja.ts | 50 +++++++++++++++++++++++++++++++++++++++ src/languages/nl.ts | 51 ++++++++++++++++++++++++++++++++++++++++ src/languages/pl.ts | 51 ++++++++++++++++++++++++++++++++++++++++ src/languages/pt-BR.ts | 51 ++++++++++++++++++++++++++++++++++++++++ src/languages/zh-hans.ts | 50 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 406 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 4de93e2aae81..f04a1c096d67 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7575,6 +7575,57 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `${fieldType}-Berichtsfeld „${fieldName}“${defaultValue ? ` mit Standardwert „${defaultValue}“` : ''} hinzugefügt`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'aktiviert' : 'deaktiviert'} die Anforderung für Firmenkartenkäufe`, + expensifyCardRule: { + actionVerb: {block: 'blockiert', allow: 'erlaubt'}, + amountOperator: {over: 'über', under: 'unter'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `Beträge ${operator} ${amount}`, + theCard: 'die Karte', + multipleCards: ({count}: {count: number}) => `${count} Karten`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} und ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')} und ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '} auf ${cards}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `Ausgaberegel von ${cards} entfernt` : 'Ausgaberegel entfernt'), + restrictionVerb: {block: 'Block', allow: 'nur zulassen'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `Ausgabenregel von ${fromAction} zu ${toAction} auf ${cards} geändert` : `Ausgabenregel von ${fromAction} in ${toAction} geändert`, + appliedToAdditionalCards: ({count}: {count: number}) => `Ausgaberegel auf ${count} zusätzliche Karten angewendet`, + phraseVerb: {added: 'hinzugefügt', removed: 'entfernt', changed: 'geändert', set: 'festlegen', applied: 'angewendet'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} Händler „${value}“` : `Händler*in „${value}“`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} Händler von „${oldValue}“ zu „${newValue}“` : `Händler*in von „${oldValue}“ zu „${newValue}“`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => + adjective !== '' ? `${adjective} Ausgabenkategorie „${value}“` : `Ausgabenkategorie „${value}“`, + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} Ausgabenkategorie von „${oldValue}“ zu „${newValue}“` : `Ausgabenkategorie von „${oldValue}“ auf „${newValue}“`, + bodyMaxAmount: 'Maximalbetrag', + bodyMaxAmountSet: ({value}: {value: string}) => `Maximalbetrag bis ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `Maximalbetrag von ${oldValue} auf ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `Ausgabenregel auf ${count} weitere Karten anwenden`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `Ausgabelimit von ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} auf ${cards}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} von ${cards}` : content), + }, + }, }, roomMembersPage: { memberNotFound: 'Mitglied nicht gefunden.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 580833a8046c..ba2d1f186201 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7597,6 +7597,57 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `a ajouté le champ de note de frais ${fieldType} « ${fieldName} »${defaultValue ? ` avec la valeur par défaut « ${defaultValue} »` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'activé' : 'désactivé'} l’exigence d’achats par carte d’entreprise`, + expensifyCardRule: { + actionVerb: {block: 'bloqué', allow: 'autorisé'}, + amountOperator: {over: 'terminé', under: 'sous'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `montants ${operator} ${amount}`, + theCard: 'la carte', + multipleCards: ({count}: {count: number}) => `${count} cartes`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} et ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')}, et ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '} sur ${cards}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `règle de dépense supprimée de ${cards}` : 'règle de dépense supprimée'), + restrictionVerb: {block: 'bloquer', allow: 'autoriser uniquement'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `a modifié la règle de dépense de ${fromAction} à ${toAction} sur ${cards}` : `a modifié la règle de dépense de ${fromAction} à ${toAction}`, + appliedToAdditionalCards: ({count}: {count: number}) => `règle de dépense appliquée à ${count} cartes supplémentaires`, + phraseVerb: {added: 'ajouté', removed: 'supprimé', changed: 'modifié', set: 'définir', applied: 'appliqué'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Commerçant·e ${adjective} « ${value} »` : `commerçant « ${value} »`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} commerçant de « ${oldValue} » à « ${newValue} »` : `commerçant de « ${oldValue} » à « ${newValue} »`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => + adjective !== '' ? `Catégorie de dépense ${adjective} « ${value} »` : `catégorie de dépense « ${value} »`, + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `Catégorie de dépense ${adjective} de « ${oldValue} » à « ${newValue} »` : `catégorie de dépense de « ${oldValue} » à « ${newValue} »`, + bodyMaxAmount: 'montant maximal', + bodyMaxAmountSet: ({value}: {value: string}) => `montant maximal à ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `montant maximum de ${oldValue} à ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `règle de dépense pour ${count} cartes supplémentaires`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `règle de dépense à partir de ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} sur ${cards}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} depuis ${cards}` : content), + }, + }, }, roomMembersPage: { memberNotFound: 'Membre introuvable.', diff --git a/src/languages/it.ts b/src/languages/it.ts index 85412c276f1f..0566007034bc 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7565,6 +7565,57 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `aggiunto campo di report ${fieldType} "${fieldName}"${defaultValue ? ` con valore predefinito "${defaultValue}"` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'abilitato' : 'disabilitato'} il requisito per gli acquisti con carta aziendale`, + expensifyCardRule: { + actionVerb: {block: 'bloccato', allow: 'consentito'}, + amountOperator: {over: 'terminato', under: 'sotto'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `importi ${operator} ${amount}`, + theCard: 'la carta', + multipleCards: ({count}: {count: number}) => `${count} carte`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} e ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')} e ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '} su ${cards}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `ha rimosso la regola di spesa da ${cards}` : 'regola di spesa rimossa'), + restrictionVerb: {block: 'bloc', allow: 'consenti solo'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `ha modificato la regola di spesa da ${fromAction} a ${toAction} su ${cards}` : `ha modificato la regola di spesa da ${fromAction} a ${toAction}`, + appliedToAdditionalCards: ({count}: {count: number}) => `regola di spesa applicata a ${count} carte aggiuntive`, + phraseVerb: {added: 'aggiunto', removed: 'rimosso', changed: 'modificato', set: 'imposta', applied: 'applicato'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} esercente '${value}'` : `esercente '${value}'`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} esercente da '${oldValue}' a '${newValue}'` : `esercente da '${oldValue}' a '${newValue}'`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => + adjective !== '' ? `categoria di spesa ${adjective} "${value}"` : `categoria di spesa '${value}'`, + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `categoria di spesa ${adjective} da '${oldValue}' a '${newValue}'` : `categoria di spesa da '${oldValue}' a '${newValue}'`, + bodyMaxAmount: 'importo massimo', + bodyMaxAmountSet: ({value}: {value: string}) => `importo massimo pari a ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `importo massimo da ${oldValue} a ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regola di spesa per ${count} carte aggiuntive`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `regola di spesa da ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} su ${cards}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} da ${cards}` : content), + }, + }, }, roomMembersPage: { memberNotFound: 'Membro non trovato.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 35666c340a22..cd88a35a8a63 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7474,6 +7474,56 @@ ${reportName} addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `${fieldType}レポートフィールド「${fieldName}」を追加しました${defaultValue ? ` デフォルト値「${defaultValue}」付き` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? '有効' : '無効'} の法人カード購入要件`, + expensifyCardRule: { + actionVerb: {block: 'ブロック済み', allow: '許可済み'}, + amountOperator: {over: '終了', under: '以下の条件のもと'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金額 ${operator} ${amount}`, + theCard: 'カード', + multipleCards: ({count}: {count: number}) => `${count} 件のカード`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} と ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')}、${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${cards}での${text === '' ? '' : ' '}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `${cards} から支出ルールを削除しました` : '支出ルールを削除しました'), + restrictionVerb: {block: 'ブロック', allow: 'のみ許可'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `${cards} の支出ルールを ${fromAction} から ${toAction} に変更しました` : `支出ルールを${fromAction}から${toAction}に変更しました`, + appliedToAdditionalCards: ({count}: {count: number}) => `${count} 枚の追加カードに支出ルールを適用しました`, + phraseVerb: {added: '追加済み', removed: '削除済み', changed: '変更済み', set: '設定', applied: '適用済み'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}なマーチャント「${value}」` : `加盟店「${value}」`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective}加盟店を「${oldValue}」から「${newValue}」に変更しました` : `加盟店名を「${oldValue}」から「${newValue}」に変更`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective}支出カテゴリー「${value}」` : `支出カテゴリ「${value}」`), + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective}支出カテゴリを「${oldValue}」から「${newValue}」に変更しました` : `支出カテゴリを「${oldValue}」から「${newValue}」に変更`, + bodyMaxAmount: '最大金額', + bodyMaxAmountSet: ({value}: {value: string}) => `最大金額を${value}に設定`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最大金額を${oldValue}から${newValue}に変更`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `${count} 枚の追加カードに支出ルールを適用`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `${cards}の支出ルール`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${cards}の${content}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${cards} からの ${content}` : content), + }, + }, }, roomMembersPage: { memberNotFound: 'メンバーが見つかりません。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 11864eee56d8..67bd8a6ec2c1 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7539,6 +7539,57 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `heeft ${fieldType}-rapportveld "${fieldName}" toegevoegd${defaultValue ? ` met standaardwaarde "${defaultValue}"` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `vereiste ${enabled ? 'ingeschakeld' : 'uitgeschakeld'} voor bedrijfskaarttransacties`, + expensifyCardRule: { + actionVerb: {block: 'geblokkeerd', allow: 'toegestaan'}, + amountOperator: {over: 'over', under: 'onder'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `bedragen ${operator} ${amount}`, + theCard: 'de kaart', + multipleCards: ({count}: {count: number}) => `${count} kaarten`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} en ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')} en ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '}op ${cards}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `uitgavenregel verwijderd van ${cards}` : 'uitgave-regel verwijderd'), + restrictionVerb: {block: 'blokkeren', allow: 'alleen toestaan'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `uitgave-regel gewijzigd van ${fromAction} naar ${toAction} op ${cards}` : `heeft bestedingsregel gewijzigd van ${fromAction} naar ${toAction}`, + appliedToAdditionalCards: ({count}: {count: number}) => `bestedingsregel toegepast op ${count} extra kaarten`, + phraseVerb: {added: 'toegevoegd', removed: 'verwijderd', changed: 'gewijzigd', set: 'instellen', applied: 'toegepast'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} handelaar '${value}'` : `handelaar '${value}'`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} handelaar van '${oldValue}' naar '${newValue}'` : `handelaar van '${oldValue}' naar '${newValue}'`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => + adjective !== '' ? `${adjective} uitgavencategorie '${value}'` : `uitgavencategorie '${value}'`, + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} uitgavencategorie van '${oldValue}' naar '${newValue}'` : `uitgavecategorie van '${oldValue}' naar '${newValue}'`, + bodyMaxAmount: 'max. bedrag', + bodyMaxAmountSet: ({value}: {value: string}) => `max. bedrag tot ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `max. bedrag van ${oldValue} naar ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `bestedsregel naar ${count} extra kaarten`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `bestedingsregel van ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} op ${cards}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} van ${cards}` : content), + }, + }, }, roomMembersPage: { memberNotFound: 'Lid niet gevonden.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index fee79580a133..8442dae3cefc 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7530,6 +7530,57 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `dodano pole raportu typu ${fieldType} „${fieldName}”${defaultValue ? ` z domyślną wartością „${defaultValue}”` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'włączone' : 'wyłączone'} wymóg dotyczący zakupów kartą służbową`, + expensifyCardRule: { + actionVerb: {block: 'zablokowano', allow: 'dozwolone'}, + amountOperator: {over: 'ponad', under: 'pod'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `kwoty ${operator} ${amount}`, + theCard: 'karta', + multipleCards: ({count}: {count: number}) => `${count} karty`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} i ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')} i ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '} na ${cards}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `usunięto regułę wydatków z ${cards}` : 'usunięto regułę wydatków'), + restrictionVerb: {block: 'zablokuj', allow: 'zezwól tylko'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `zmieniono regułę wydatków z ${fromAction} na ${toAction} na ${cards}` : `zmienił(a) regułę wydatków z ${fromAction} na ${toAction}`, + appliedToAdditionalCards: ({count}: {count: number}) => `zastosowano regułę wydatków do ${count} dodatkowych kart`, + phraseVerb: {added: 'dodano', removed: 'usunięto', changed: 'zmieniono', set: 'ustaw', applied: 'zastosowano'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} sprzedawca „${value}”` : `sprzedawca „${value}”`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} sprzedawcę z „${oldValue}” na „${newValue}”` : `sprzedawcę z „${oldValue}” na „${newValue}”`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => + adjective !== '' ? `kategoria wydatków ${adjective} „${value}”` : `kategoria wydatków „${value}”`, + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} kategorię wydatków z „${oldValue}” na „${newValue}”` : `kategoria wydatku z „${oldValue}” na „${newValue}”`, + bodyMaxAmount: 'maksymalna kwota', + bodyMaxAmountSet: ({value}: {value: string}) => `maksymalna kwota do ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `maksymalna kwota z ${oldValue} na ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `zasada wydatków dla ${count} dodatkowych kart`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `reguła wydatków z ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} na ${cards}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} z ${cards}` : content), + }, + }, }, roomMembersPage: { memberNotFound: 'Nie znaleziono członka.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index d44b4ef42fa6..c3b7bdfb51c2 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7531,6 +7531,57 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `adicionou o campo de relatório ${fieldType} "${fieldName}"${defaultValue ? ` com valor padrão "${defaultValue}"` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? 'ativado' : 'desativado'} o requisito de compras com cartão corporativo`, + expensifyCardRule: { + actionVerb: {block: 'bloqueado', allow: 'permitido'}, + amountOperator: {over: 'sobre', under: 'abaixo'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `valores ${operator} ${amount}`, + theCard: 'o cartão', + multipleCards: ({count}: {count: number}) => `${count} cartões`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} e ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')}, e ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${text === '' ? '' : ' '}em ${cards}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `removeu a regra de gasto de ${cards}` : 'removeu a regra de gasto'), + restrictionVerb: {block: 'bloquear', allow: 'permitir somente'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `alterou a regra de gasto de ${fromAction} para ${toAction} em ${cards}` : `alterou a regra de gasto de ${fromAction} para ${toAction}`, + appliedToAdditionalCards: ({count}: {count: number}) => `regra de gasto aplicada a mais ${count} cartões`, + phraseVerb: {added: 'adicionado', removed: 'removido', changed: 'alterado', set: 'definir', applied: 'aplicado'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `Comerciante ${adjective} '${value}'` : `estabelecimento '${value}'`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `${adjective} comerciante de '${oldValue}' para '${newValue}'` : `estabelecimento comercial de '${oldValue}' para '${newValue}'`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => + adjective !== '' ? `categoria de gasto ${adjective} '${value}'` : `categoria de despesa '${value}'`, + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `categoria de gasto ${adjective} de '${oldValue}' para '${newValue}'` : `categoria de gasto de '${oldValue}' para '${newValue}'`, + bodyMaxAmount: 'valor máximo', + bodyMaxAmountSet: ({value}: {value: string}) => `valor máximo de ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `valor máximo de ${oldValue} para ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `regra de gasto para ${count} cartões adicionais`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `regra de gasto de ${cards}`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} em ${cards}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${content} de ${cards}` : content), + }, + }, }, roomMembersPage: { memberNotFound: 'Membro não encontrado.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index d416e61161e1..4db9fce46503 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7344,6 +7344,56 @@ ${reportName} `已更改卡片流水“${feedName}”的账单周期截止日${newValue ? ` 为“${newValue}”` : ''}${previousValue ? ` (先前为“${previousValue}”)` : ''}`, addedReportField: (fieldType: string, fieldName?: string, defaultValue?: string) => `已添加 ${fieldType} 报告字段“${fieldName}”${defaultValue ? ` 默认值为“${defaultValue}”` : ''}`, updatedRequireCompanyCards: ({enabled}: {enabled: boolean}) => `${enabled ? '已启用' : '已禁用'} 公司商务卡消费要求`, + expensifyCardRule: { + actionVerb: {block: '已阻止', allow: '允许'}, + amountOperator: {over: '结束', under: '在…之下'}, + amountFilter: ({operator, amount}: {operator: string; amount: string}) => `金额 ${operator} ${amount}`, + theCard: '该卡', + multipleCards: ({count}: {count: number}) => `${count} 张卡片`, + joinFilters: ({items}: {items: string[]}) => { + if (items.length === 0) { + return ''; + } + if (items.length === 1) { + return items.at(0) ?? ''; + } + if (items.length === 2) { + return `${items.at(0)} 和 ${items.at(1)}`; + } + return `${items.slice(0, -1).join(', ')},和 ${items.at(-1)}`; + }, + addRule: ({verb, filters, cards}: {verb: string; filters: string; cards: string}) => { + let text = verb; + if (filters !== '') { + text += `${text === '' ? '' : ' '}${filters}`; + } + if (cards !== '') { + text += `${cards} 上的 ${text === '' ? '' : ' '}`; + } + return text; + }, + removeRule: ({cards}: {cards: string}) => (cards !== '' ? `已从${cards}中移除消费规则` : '已移除消费规则'), + restrictionVerb: {block: '封锁', allow: '仅允许'}, + update: { + modeChange: ({fromAction, toAction, cards}: {fromAction: string; toAction: string; cards: string}) => + cards !== '' ? `已将 ${cards} 的消费规则从 ${fromAction} 更改为 ${toAction}` : `将消费规则从 ${fromAction} 更改为 ${toAction}`, + appliedToAdditionalCards: ({count}: {count: number}) => `已将消费规则应用到另外 ${count} 张卡片`, + phraseVerb: {added: '已添加', removed: '已移除', changed: '已更改', set: '设置', applied: '已应用'}, + bodyMerchant: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} 商户“${value}”` : `商户“${value}”`), + bodyMerchantChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `将${adjective}商户从“${oldValue}”更改为“${newValue}”` : `商户从“${oldValue}”变更为“${newValue}”`, + bodySpendCategory: ({adjective, value}: {adjective: string; value: string}) => (adjective !== '' ? `${adjective} 支出类别“${value}”` : `支出类别“${value}”`), + bodySpendCategoryChange: ({adjective, oldValue, newValue}: {adjective: string; oldValue: string; newValue: string}) => + adjective !== '' ? `将${adjective}支出类别从“${oldValue}”更改为“${newValue}”` : `将支出类别从“${oldValue}”更改为“${newValue}”`, + bodyMaxAmount: '最高金额', + bodyMaxAmountSet: ({value}: {value: string}) => `最大金额为 ${value}`, + bodyMaxAmountChange: ({oldValue, newValue}: {oldValue: string; newValue: string}) => `最大金额从 ${oldValue} 变更为 ${newValue}`, + bodyAppliedToAdditionalCards: ({count}: {count: number}) => `将消费规则应用到另外 ${count} 张卡片`, + bodyRemovedFromCards: ({cards}: {cards: string}) => `来自${cards}的支出规则`, + composeOnCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `${cards} 上的 ${content}` : content), + composeFromCards: ({content, cards}: {content: string; cards: string}) => (cards !== '' ? `来自${cards}的${content}` : content), + }, + }, }, roomMembersPage: { memberNotFound: '未找到成员。', From ac3d0d1f9e4ae64627e516c9dd8415c5a78f12af Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 16:36:49 -0300 Subject: [PATCH 16/17] fix types --- src/libs/ReportActionsUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 46ebd309b0de..a1cca596a264 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -4250,8 +4250,8 @@ function getUpdateExpensifyCardRuleMessage(translate: LocalizedTranslate, report } if (amountDiff.added.length === 1 && amountDiff.removed.length === 1) { - const oldValue = spendRuleFormatAmountValue(amountDiff.removed.at(0) ?? {}, currency); - const newValue = spendRuleFormatAmountValue(amountDiff.added.at(0) ?? {}, currency); + const oldValue = spendRuleFormatAmountValue(amountDiff.removed.at(0) ?? {value: []}, currency); + const newValue = spendRuleFormatAmountValue(amountDiff.added.at(0) ?? {value: []}, currency); const body = translate('workspaceActions.expensifyCardRule.update.bodyMaxAmountChange', {oldValue, newValue}); phrases.push({verb: 'changed', adjective: '', bodyWithAdjective: body, bodyWithoutAdjective: body}); } else { From 3c53c0656b2a57a7616190438026091770808403 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 7 May 2026 16:41:43 -0300 Subject: [PATCH 17/17] fix ts --- src/languages/en.ts | 1 - src/languages/es.ts | 1 - src/libs/ReportActionsUtils.ts | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index e9a8f2b67a1c..241973bacc65 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7284,7 +7284,6 @@ const translations = { }, amountFilter: ({operator, amount}: {operator: string; amount: string}) => `amounts ${operator} ${amount}`, theCard: 'the card', - namedCard: ({name}: {name: string}) => `'${name}'`, multipleCards: ({count}: {count: number}) => `${count} cards`, joinFilters: ({items}: {items: string[]}) => { if (items.length === 0) { diff --git a/src/languages/es.ts b/src/languages/es.ts index f47a7330e81f..5aff1d827372 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7137,7 +7137,6 @@ ${amount} para ${merchant} - ${date}`, }, amountFilter: ({operator, amount}) => `montos ${operator} ${amount}`, theCard: 'la tarjeta', - namedCard: ({name}) => `'${name}'`, multipleCards: ({count}) => `${count} tarjetas`, joinFilters: ({items}) => { if (items.length === 0) { diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index a1cca596a264..267d85cdcc33 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3938,7 +3938,7 @@ function getSpendRuleCardsSummary(translate: LocalizedTranslate, cards: Readonly } if (cards.length === 1) { const displayName = cards.at(0)?.displayName ?? ''; - return displayName !== '' ? translate('workspaceActions.expensifyCardRule.namedCard', {name: displayName}) : translate('workspaceActions.expensifyCardRule.theCard'); + return displayName !== '' ? displayName : translate('workspaceActions.expensifyCardRule.theCard'); } return translate('workspaceActions.expensifyCardRule.multipleCards', {count: cards.length}); }