whisper-money/app/Http/Requests
Víctor Falcón 275b77e640
fix(ai): stop a learned rule title from 500-ing the user's category change (#741)
## The bug

Sentry
[PHP-LARAVEL-53](https://whisper-money.sentry.io/issues/PHP-LARAVEL-53)
— `SQLSTATE[22001]: Data too long for column 'title'` on `PATCH
/transactions/{transaction}`. 6 events, 1 user.

Changing a transaction's category learns a forward-looking automation
rule, and `AiRuleLearner::title()` rebuilds the rule's title from the
merchant names it matches. This user's bank puts the **entire statement
line** in `creditor_name`:

```
15/06 12/06 Pago Con Tarjeta En Discos, Libros, Fotos Y Pc's -59,00 189,27 | 4940197152806468 Classpass* Monthly
```

Two corrections into the same category produced a 279-char title against
a `varchar(255)` column, so `$rule->save()` threw and took the whole
request with it. Since `creditor_name` is itself capped at 255 on
ingest, a single correction can overflow it too.

## The fix

1. **`AiRuleLearner::title()`** caps each merchant token (40 chars) and
the category name (60), so the title stays readable instead of merely
short — the `→ Category` tail is the part that carries the meaning.
2. **A `title` mutator on `AutomationRule`** truncates to the column
width. Both reviews flagged that fixing the learner alone leaves the
sibling generator, `ApplyRuleSuggestions::title()`, unguarded: it joins
three AI match tokens that are each validated at `max:255`, so it can
overflow the same column and 500 the accept-suggestions step **during
onboarding**. The mutator covers every generator, present and future, in
one place. User-authored titles never reach it —
`StoreAutomationRuleRequest` already rejects longer ones with a 422.
3. **Learning can no longer abort the correction.**
`CategoryOverrideHandler::record()` runs before the transaction is saved
and outside any DB transaction, so the incident left the correction
logged and the ai rules stripped while the category the user asked for
was never written. `bulkUpdate` is worse: it records every transaction
before the mass update, so one throw mutated rules for the earlier ones
and categorized none of them. A learning failure is now reported and
swallowed — both callers already handle "nothing learned".

## Tests

- `AiRuleLearnerTest` — replays the exact production payload; reproduces
the identical `SQLSTATE[22001]` on the pre-fix code and asserts the
title stays inside the column with the category still visible.
- `AutomationRuleTest` — the mutator truncates rather than failing the
write.
- `CategoryOverrideHandlerTest` — a throwing learner is reported, not
propagated.

191 tests green locally (`tests/Feature/Ai` + the three automation-rule
suites) against the MySQL testcontainer; `pint --test` clean.

## Deliberately out of scope

- **Dead-weight rules.** With such a merchant name the learned clause is
`creditor_name == "…-59,00 189,27 | …"` — it contains amounts and a
running balance, so it can never match again, yet it accumulates one
clause per correction and the UI toasts *"Learned: similar transactions
will be categorized automatically"*. The review's prod data says length
is the wrong guard (62 long merchant names DO repeat, across 490
transactions); the right one is rejecting the statement-line *signature*
(an embedded `dd/mm` or `nn,nn`) in `merchantKey()` so it falls through
to the already-guarded description-token path. Worth its own PR.
- **Rules list UI**: a long title sits in a `whitespace-nowrap` cell and
pushes the actions column off-screen (`settings/automation-rules.tsx`).
- **Unbounded writes of the same class elsewhere**, found while
reviewing: `transactions.currency_code` `varchar(3)` written unvalidated
from the Enable Banking payload (`TransactionSyncService.php:179`), and
`banking_connections.aspsp_name`/`aspsp_logo` with no `max:` rule in
`StartAuthorizationRequest`.

Fixes PHP-LARAVEL-53
2026-08-09 15:39:14 +00:00
..
Ai feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
Api feat(import): persist per-account import configuration on the backend (#698) 2026-07-18 16:10:40 +02:00
Concerns fix(balances): stop historical-balance generator OOM on ancient purchase/loan dates (PHP-LARAVEL-49) (#661) 2026-07-08 21:34:43 +00:00
OpenBanking feat(banking): add Interactive Brokers sync via Flex Web Service (#581) 2026-06-23 11:39:24 +02:00
Settings fix(ai): stop a learned rule title from 500-ing the user's category change (#741) 2026-08-09 15:39:14 +00:00
BulkReEvaluateRulesRequest.php feat(transactions): add counterparty fields (#440) 2026-05-27 16:20:55 +02:00
BulkUpdateTransactionsRequest.php refactor(requests): extract user-owned exists rules to trait (#477) 2026-06-03 17:33:54 +02:00
IndexTransactionRequest.php feat(ai): auto-categorize transactions with AI (behind flag) (#535) 2026-06-15 16:35:20 +02:00
ReorderAccountsRequest.php feat(accounts): reorder accounts with drag-and-drop (#575) 2026-06-21 11:17:45 +02:00
StoreAccountBalanceRequest.php feat: investment benefits — show gains/losses on investment accounts (#140) 2026-02-23 13:59:10 +01:00
StoreBudgetRequest.php feat: add catch-all budgets (#527) 2026-06-15 16:07:19 +00:00
StoreIntegrationRequestRequest.php feat(integration-requests): community board to request & vote bank integrations (#550) 2026-06-17 12:50:51 +00:00
StoreRealEstateDetailRequest.php refactor(requests): share account detail validation rules (#481) 2026-06-03 19:01:03 +02:00
StoreTransactionRequest.php refactor(requests): extract user-owned exists rules to trait (#477) 2026-06-03 17:33:54 +02:00
UpdateAccountVisibilityRequest.php feat(dashboard): add accounts manager dialog with visibility toggle and reorder (#604) 2026-06-27 16:11:25 +00:00
UpdateBudgetRequest.php feat(budgets): track multiple categories and labels per budget (#466) 2026-06-01 12:32:23 +02:00
UpdateCurrentAccountBalanceRequest.php feat: investment benefits — show gains/losses on investment accounts (#140) 2026-02-23 13:59:10 +01:00
UpdateLoanDetailRequest.php feat(accounts): add loan amortization projections for loan accounts (#246) 2026-03-26 15:06:09 +01:00
UpdateRealEstateDetailRequest.php refactor(requests): share account detail validation rules (#481) 2026-06-03 19:01:03 +02:00
UpdateTransactionRequest.php feat(transactions): allow editing all fields of manual transactions (#683) 2026-07-16 08:59:10 +02:00