Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e504053
Fit the Describe rule modal above the keyboard on small screens
rushatgabhane Oct 14, 2026
303dc26
Clear the last Describe prompt and error each time the modal opens
rushatgabhane Oct 14, 2026
7e6bebb
Pair receipt and itemized receipt settings in a described Require fie…
rushatgabhane Oct 14, 2026
67a07be
Reject a described rule for a category that already has one
rushatgabhane Oct 14, 2026
77aa80b
Move the spend rule eligible-card lookup into SpendRulesUtils
rushatgabhane Oct 14, 2026
c1aabf0
Select every eligible card when a described card rule covers all cards
rushatgabhane Oct 14, 2026
4c3e86d
Close only the modal when browser Back pops a modal history entry
rushatgabhane Oct 14, 2026
4dc31e8
Link the PR that adds the modal history entry patch
rushatgabhane Oct 14, 2026
5528494
Describe a category tax rule from the Category expense default editor
rushatgabhane Oct 14, 2026
e0ad73e
Drop described categories that are missing or already have a tax rule
rushatgabhane Oct 14, 2026
83e66d7
Keep a described category tax rate only when the workspace can use it
rushatgabhane Oct 14, 2026
625a2b1
Revert "Link the PR that adds the modal history entry patch"
rushatgabhane Oct 15, 2026
605e618
Revert "Close only the modal when browser Back pops a modal history e…
rushatgabhane Oct 15, 2026
9bd0b96
Keep the real SpendRulesUtils in SpendRuleCardPageTest
rushatgabhane Oct 15, 2026
63fae43
Drop a described category that already has a rule instead of showing …
rushatgabhane Oct 15, 2026
65702f1
Simplify applying a described category tax rule
rushatgabhane Oct 15, 2026
094c77a
Show an error when a described category already has a rule
rushatgabhane Oct 15, 2026
1f50809
Wait for card settings before selecting all eligible cards
rushatgabhane Oct 15, 2026
0593f10
Merge branch 'Expensify:main' into rushat-describeRuleFixes
rushatgabhane Oct 16, 2026
22db474
Merge remote-tracking branch 'upstream/main' into rushat-describeRule…
rushatgabhane Oct 19, 2026
a6a3a82
Remove unused SpendRulesUtils mock
rushatgabhane Oct 19, 2026
d3f53f6
Add docblock to setNewRulePromptErrorMessage
rushatgabhane Oct 19, 2026
8877298
Show the Describe rule modal on native by scrolling the form only on …
rushatgabhane Oct 19, 2026
0ab26b8
Keep the Describe rule form out of a scroll view
rushatgabhane Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,7 @@ const CONST = {
FLAG_FOR_REVIEW: 'flagForReview',
RESTRICT_CARD_SPEND: 'restrictCardSpend',
EXPENSE_DEFAULTS: 'expenseDefaults',
CATEGORY_TAX_RATE: 'categoryTaxRate',
},
},

Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8843,6 +8843,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
unsupported: 'Diese Regel wird noch nicht unterstützt – wir arbeiten daran. Bitte versuche vorerst eine andere Regel.',
unintelligible: 'Formuliere deine Regel etwa so: Wenn Ausgaben über 100 $ liegen, ist ein Beleg erforderlich.',
multipleRules: 'Bitte eine Regel nach der anderen – beschreibe eine einzelne Regel, z. B.: Einzelhandelskäufe auf allen Karten blockieren.',
categoryHasRule: 'Für diese Kategorie gibt es bereits eine Regel. Bearbeite die bestehende Regel oder beschreibe eine Regel für eine andere Kategorie.',
},
},
expenseDefaultsTable: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8937,6 +8937,7 @@ ${reportName}`,
unsupported: 'Αυτός ο κανόνας δεν υποστηρίζεται ακόμη — το δουλεύουμε. Δοκιμάστε προς το παρόν έναν διαφορετικό κανόνα.',
unintelligible: 'Δοκιμάστε να διατυπώσετε τον κανόνα σας έτσι: αν οι δαπάνες ξεπερνούν τα 100 $, να απαιτείται απόδειξη.',
multipleRules: 'Έναν κανόνα τη φορά — δοκιμάστε να περιγράψετε έναν μόνο κανόνα, π.χ.: αποκλεισμός αγορών λιανικής σε όλες τις κάρτες.',
categoryHasRule: 'Αυτή η κατηγορία έχει ήδη κανόνα. Επεξεργαστείτε τον υπάρχοντα κανόνα ή περιγράψτε έναν κανόνα για άλλη κατηγορία.',
},
},
expenseDefaultsTable: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9171,6 +9171,7 @@ const translations = {
unsupported: "That rule isn't supported yet — we're working on it. Please try a different rule for now.",
unintelligible: 'Try phrasing your rule like: if expenses have a value over $100, then require a receipt.',
multipleRules: 'One rule at a time, please — try describing a single rule, like: block retail purchases on all cards.',
categoryHasRule: 'That category already has a rule. Edit the existing rule or describe a rule for another category.',
},
},
expenseDefaultsTable: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8848,6 +8848,7 @@ ${reportName}`,
unsupported: 'Esa regla aún no es compatible: estamos trabajando en ello. Prueba con otra regla por ahora.',
unintelligible: 'Prueba a formular tu regla así: si los gastos superan los $100, exige un recibo.',
multipleRules: 'Una regla a la vez, por favor. Prueba a describir una sola regla, como: bloquear compras en comercios minoristas en todas las tarjetas.',
categoryHasRule: 'Esa categoría ya tiene una regla. Edita la regla existente o describe una regla para otra categoría.',
},
},
expenseDefaultsTable: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8870,6 +8870,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
unsupported: 'Cette règle n’est pas encore prise en charge — nous y travaillons. Essayez une autre règle pour le moment.',
unintelligible: 'Essayez de formuler votre règle ainsi : si les dépenses dépassent 100 $, exigez un reçu.',
multipleRules: 'Une règle à la fois, s’il vous plaît — essayez de décrire une seule règle, comme : bloquer les achats en magasin sur toutes les cartes.',
categoryHasRule: 'Cette catégorie a déjà une règle. Modifiez la règle existante ou décrivez une règle pour une autre catégorie.',
},
},
expenseDefaultsTable: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8794,6 +8794,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
unsupported: 'Questa regola non è ancora supportata: ci stiamo lavorando. Per ora prova una regola diversa.',
unintelligible: 'Prova a formulare la regola così: se le spese superano i 100 $, richiedi una ricevuta.',
multipleRules: 'Una regola alla volta, per favore: prova a descrivere una singola regola, ad esempio bloccare gli acquisti al dettaglio su tutte le carte.',
categoryHasRule: "Questa categoria ha già una regola. Modifica la regola esistente o descrivi una regola per un'altra categoria.",
},
},
expenseDefaultsTable: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8683,6 +8683,7 @@ ${reportName}`,
unsupported: 'そのルールはまだ対応していません。現在対応を進めています。ほかのルールをお試しください。',
unintelligible: '「経費が100ドルを超える場合は領収書を必須にする」のように書いてみてください。',
multipleRules: '一度に1つのルールのみです。「すべてのカードで小売店での購入をブロックする」のように、1つのルールを説明してください。',
categoryHasRule: 'このカテゴリにはすでにルールがあります。既存のルールを編集するか、別のカテゴリのルールを説明してください。',
},
},
expenseDefaultsTable: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8768,6 +8768,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
unsupported: 'Die regel wordt nog niet ondersteund — we werken eraan. Probeer voorlopig een andere regel.',
unintelligible: 'Formuleer je regel bijvoorbeeld zo: als uitgaven hoger zijn dan $100, vraag dan een bon.',
multipleRules: 'Eén regel per keer graag — beschrijf één enkele regel, bijvoorbeeld: retailaankopen op alle kaarten blokkeren.',
categoryHasRule: 'Deze categorie heeft al een regel. Bewerk de bestaande regel of beschrijf een regel voor een andere categorie.',
},
},
expenseDefaultsTable: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8793,6 +8793,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
unsupported: 'Ta reguła nie jest jeszcze obsługiwana — pracujemy nad tym. Na razie spróbuj innej reguły.',
unintelligible: 'Spróbuj sformułować regułę tak: jeśli wydatek przekracza 100 $, wymagaj paragonu.',
multipleRules: 'Jedna reguła naraz — opisz pojedynczą regułę, na przykład: zablokuj zakupy detaliczne na wszystkich kartach.',
categoryHasRule: 'Ta kategoria ma już regułę. Edytuj istniejącą regułę lub opisz regułę dla innej kategorii.',
},
},
expenseDefaultsTable: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8775,6 +8775,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
unsupported: 'Essa regra ainda não tem suporte — estamos trabalhando nisso. Tente uma regra diferente por enquanto.',
unintelligible: 'Tente formular sua regra assim: se a despesa for maior que $100, exija um recibo.',
multipleRules: 'Uma regra por vez, por favor — tente descrever uma única regra, como: bloquear compras no varejo em todos os cartões.',
categoryHasRule: 'Essa categoria já tem uma regra. Edite a regra existente ou descreva uma regra para outra categoria.',
},
},
expenseDefaultsTable: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8465,6 +8465,7 @@ ${reportName}`,
unsupported: '该规则暂不支持,我们正在开发中。请先尝试其他规则。',
unintelligible: '可以这样描述规则:如果支出超过 100 美元,则需要收据。',
multipleRules: '请一次描述一条规则,例如:在所有卡上阻止零售消费。',
categoryHasRule: '该类别已有规则。请编辑现有规则,或为其他类别描述规则。',
},
},
expenseDefaultsTable: {
Expand Down
43 changes: 40 additions & 3 deletions src/libs/SpendRulesUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import type {LocalizedTranslate} from '@components/LocaleContextProvider';
import CONST from '@src/CONST';
import type {SpendRuleForm} from '@src/types/form';
import {isSpendRuleCategory} from '@src/types/form/SpendRuleForm';
import type {ExpensifyCardSettings} from '@src/types/onyx';
import type {Card, ExpensifyCardSettings, WorkspaceCardsList} from '@src/types/onyx';
import type {ExpensifyCardRule, ExpensifyCardRuleFilter} from '@src/types/onyx/ExpensifyCardSettings';

import type {OnyxCollection} from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';

import {convertToBackendAmount} from './CurrencyUtils';
Expand Down Expand Up @@ -194,6 +194,35 @@ type SpendRuleByCardID = {
formValues: SpendRuleForm;
};

function getCardIDsWithSpendRules(cardRules: Record<string, ExpensifyCardRule> | undefined, currentRuleID?: string): Set<number> {
const cardIDs = new Set<number>();
if (!cardRules) {
return cardIDs;
}

for (const [ruleID, rule] of Object.entries(cardRules)) {
if (ruleID === currentRuleID) {
continue;
}
Comment thread
JS00001 marked this conversation as resolved.
const formValues = getSpendRuleFormValuesFromCardRule(rule);
for (const cardID of formValues?.cardIDs ?? []) {
const numericCardID = Number(cardID);
if (Number.isFinite(numericCardID)) {
Comment thread
JS00001 marked this conversation as resolved.
cardIDs.add(numericCardID);
}
}
}

return cardIDs;
}

/** Returns the cards a spend rule can cover, which are the cards no other spend rule covers */
function getEligibleSpendRuleCards(cardsList: OnyxEntry<WorkspaceCardsList>, expensifyCardSettings: OnyxEntry<ExpensifyCardSettings>, currentRuleID?: string): Card[] {
const {cardList, ...cards} = cardsList ?? {};
const cardIDsWithSpendRules = getCardIDsWithSpendRules(expensifyCardSettings?.cardRules, currentRuleID);
return Object.values(cards).filter((card: Card) => !cardIDsWithSpendRules.has(card.cardID));
}

/** At most one custom spend rule should reference a card; returns rule id + form values, or undefined. */
function getSpendRuleByCardID(expensifyCardSettingsCollection: OnyxCollection<ExpensifyCardSettings> | undefined, cardID: string): SpendRuleByCardID | undefined {
for (const settings of Object.values(expensifyCardSettingsCollection ?? {})) {
Expand Down Expand Up @@ -356,4 +385,12 @@ function getSpendRuleSummaryText(
}

export type {SpendRuleSummaryPart};
export {buildSpendRuleAST, getSpendRuleByCardID, getSpendRuleFormValuesFromCardRule, getSpendRuleSummaryParts, getSpendRuleSummaryText, getTruncatedSpendRuleSummary};
export {
buildSpendRuleAST,
getEligibleSpendRuleCards,
getSpendRuleByCardID,
getSpendRuleFormValuesFromCardRule,
getSpendRuleSummaryParts,
getSpendRuleSummaryText,
getTruncatedSpendRuleSummary,
};
8 changes: 7 additions & 1 deletion src/libs/actions/Policy/Rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,9 +447,14 @@ function generateRule(policyID: string, ruleType: GeneratedRuleType, prompt: str

/** Shows on the prompt form why a description did not become a rule */
function setNewRulePromptError(state: GeneratedRuleState) {
setNewRulePromptErrorMessage(PROMPT_ERROR_BY_STATE[state] ?? 'common.genericErrorMessage');
}

/** Shows an error on the prompt form */
function setNewRulePromptErrorMessage(translationKey: TranslationPaths) {
Comment thread
rushatgabhane marked this conversation as resolved.
Onyx.merge(ONYXKEYS.FORMS.NEW_RULE_PROMPT_FORM, {
isLoading: false,
errors: getMicroSecondOnyxErrorWithTranslationKey(PROMPT_ERROR_BY_STATE[state] ?? 'common.genericErrorMessage'),
errors: getMicroSecondOnyxErrorWithTranslationKey(translationKey),
});
}

Expand Down Expand Up @@ -728,6 +733,7 @@ export {
addPolicyAgentRule,
generateRule,
setNewRulePromptError,
setNewRulePromptErrorMessage,
clearGeneratedRule,
clearNewRulePrompt,
updatePolicyAgentRule,
Expand Down
18 changes: 17 additions & 1 deletion src/pages/workspace/rules/DescribeRuleButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';

import {clearNewRulePrompt} from '@userActions/Policy/Rules';

import CONST from '@src/CONST';
import type {PolicyCategory} from '@src/types/onyx';
import type {GeneratedRuleType, GeneratedRuleValues} from '@src/types/onyx/GeneratedRule';

import React, {useState} from 'react';
Expand All @@ -27,6 +30,9 @@ type DescribeRuleButtonProps = {
/** Called with the generated form values once Concierge has built the rule */
onRuleGenerated: (values: GeneratedRuleValues) => void;

/** Whether a new rule can use the category, so a generated category that already has this kind of rule shows an error */
isCategoryEligible?: (category: PolicyCategory) => boolean;

onPress?: never;
}
| {
Expand All @@ -46,12 +52,21 @@ function DescribeRuleButton(props: DescribeRuleButtonProps) {
return null;
}

const openModal = () => {
clearNewRulePrompt();
if (props.onPress) {
props.onPress();
return;
}
setIsModalVisible(true);
};

return (
<>
<Button
size={CONST.BUTTON_SIZE.LARGE}
style={styles.flex1}
onPress={props.onPress ?? (() => setIsModalVisible(true))}
onPress={openModal}
sentryLabel={props.sentryLabel}
>
<Button.Text>{translate('workspace.rules.newRule.describe')}</Button.Text>
Expand All @@ -63,6 +78,7 @@ function DescribeRuleButton(props: DescribeRuleButtonProps) {
policyID={props.policyID}
ruleType={props.ruleType}
onRuleGenerated={props.onRuleGenerated}
isCategoryEligible={props.isCategoryEligible}
/>
)}
</>
Expand Down
Loading
Loading