fix(automation-rules): hint amount sign for expenses vs income (#524)

## Problem

A user reported automation rules not applying. The rule matched a
description **and** `Valor es igual a 21,99`, but never fired.

Root cause: the matching engine evaluates the **signed** transaction
amount (`amount / 100`), and expenses are stored negative. So an expense
of `-21,99` is compared as `-21.99 == 21.99` → false. The rule editor
showed a plain number input with no hint about the sign convention, so
entering a positive `21,99` to match an expense silently builds a rule
that can never match.

This is a common trap — likely affecting many users with amount-based
rules.

## Change

Add a small helper note under the numeric `amount`/`Valor` value input:

> Usa un valor negativo para gastos (ej. -21,99) y un valor positivo
para ingresos.

- Renders only for the numeric `amount` field; text conditions are
unaffected.
- No change to how rules are stored or evaluated — existing rules keep
working.
- Added the Spanish translation key (`es.json` is CI-enforced).

## Testing

- `bun run format` and `eslint` clean on the component.
- `LocalizationTest` passes (translation key resolves, no missing keys).

## Follow-up (not in this PR)

The hint guides new rules but doesn't retroactively fix rules already
saved wrong. A future enhancement could warn when saving a
positive-value amount condition, since most finance rules target
expenses.
This commit is contained in:
Víctor Falcón 2026-06-12 19:04:43 +02:00 committed by GitHub
parent d2a4412118
commit e526f861b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 70 additions and 52 deletions

View File

@ -1645,6 +1645,7 @@
"Uruguayan Peso": "Peso uruguayo",
"Usage Information:": "Información de Uso:",
"Use Defaults": "Usar Valores Predeterminados",
"Use a negative value for expenses (e.g. -21.99) and a positive value for income.": "Usa un valor negativo para gastos (ej. -21,99) y un valor positivo para ingresos.",
"Use a strong password (minimum 12 characters). This password will encrypt your data.": "Usa una contraseña fuerte (mínimo 12 caracteres). Esta contraseña encriptará tus datos.",
"Use a strong password (minimum 12 characters). This\\n password will encrypt your data.": "Usa una contraseña segura (mínimo 12 caracteres). Esta contraseña cifrará tus datos.",
"Use a view-only key.": "Usa una clave de solo lectura.",

View File

@ -275,62 +275,79 @@ function ConditionRow({
const inputType = fieldConfig?.type === 'number' ? 'number' : 'text';
const showAmountHint = showValueInput && inputType === 'number';
return (
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Select value={condition.field} onValueChange={handleFieldChange}>
<SelectTrigger className="w-full sm:w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(FIELD_CONFIG).map(([key, config]) => (
<SelectItem key={key} value={key}>
{__(config.label)}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Select
value={condition.field}
onValueChange={handleFieldChange}
>
<SelectTrigger className="w-full sm:w-[180px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(FIELD_CONFIG).map(([key, config]) => (
<SelectItem key={key} value={key}>
{__(config.label)}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={condition.operator}
onValueChange={handleOperatorChange}
>
<SelectTrigger className="w-full sm:w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{availableOperators.map((op) => (
<SelectItem key={op} value={op}>
{__(OPERATOR_LABELS[op])}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={condition.operator}
onValueChange={handleOperatorChange}
>
<SelectTrigger className="w-full sm:w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{availableOperators.map((op) => (
<SelectItem key={op} value={op}>
{__(OPERATOR_LABELS[op])}
</SelectItem>
))}
</SelectContent>
</Select>
{showValueInput && (
<Input
type={inputType}
value={condition.value}
onChange={(e) =>
onChange({ ...condition, value: e.target.value })
}
placeholder={__('Value')}
className="w-full sm:flex-1"
step={inputType === 'number' ? 'any' : undefined}
/>
{showValueInput && (
<Input
type={inputType}
value={condition.value}
onChange={(e) =>
onChange({ ...condition, value: e.target.value })
}
placeholder={__('Value')}
className="w-full sm:flex-1"
step={inputType === 'number' ? 'any' : undefined}
/>
)}
{!showValueInput && (
<div className="hidden sm:flex sm:flex-1" />
)}
<Button
type="button"
variant="ghost"
size="sm"
onClick={onRemove}
disabled={!canRemove}
className={`self-end sm:self-auto${!canRemove ? 'opacity-30' : ''}`}
>
<X className="h-4 w-4" />
</Button>
</div>
{showAmountHint && (
<p className="text-xs pl-2 py-2 text-muted-foreground">
{__(
'Use a negative value for expenses (e.g. -21.99) and a positive value for income.',
)}
</p>
)}
{!showValueInput && <div className="hidden sm:flex sm:flex-1" />}
<Button
type="button"
variant="ghost"
size="sm"
onClick={onRemove}
disabled={!canRemove}
className={`self-end sm:self-auto${!canRemove ? 'opacity-30' : ''}`}
>
<X className="h-4 w-4" />
</Button>
</div>
);
}