whisper-money/tests/Feature/Console
Víctor Falcón b35968b456
fix(banking): strip the ISO 20022 remittance tag from transaction descriptions (#744)
## Why

A support ticket: Bankinter transactions arrive with the raw ISO 20022
remittance tag in front of the text, so AI categorization reads the tag
instead of the merchant.

```
/TXT/D|SumUp *GELATERIA SALV
/TXT/H|TRANSF NOMI /AIGUA DE RIGAT, S      ← the payroll that got categorized as Fuel
/TXT/CONDIS SANT JUST DESV07/07/26|20260714
```

`/TXT/` is the unstructured remittance tag, `D|`/`H|` is the debe/haber
marker (which only repeats the sign of the amount), and card payments
append the purchase and settlement dates. None of it describes the
transaction.

In production this hits **7297 transactions across 20 users** —
Bankinter (6569) and Unicaja Banco (728), which ships the same shape.

## What

**`RemittanceTagFormatter`** strips the tag, the credit/debit marker,
the card dates and the `#` marker on card charges. The tag identifies
itself, so the formatter is keyed on the **description** rather than on
a bank name — it works for any bank shipping the same shape instead of
needing a new class per bank. `BankFormatter::matches()` now takes the
description as well; `BbvaFormatter` keeps matching on the bank name.

**`banking:backfill-descriptions`** fixes the rows that are already
imported. It also rewrites the automation rules that match on the raw
text: **48 user-authored rules across 4 users** contain literals like
`/TXT/D|RECIBO VISA CLASICA`, and rewriting descriptions without
rewriting those rules would silently stop them from ever matching again.
A test pins that a rule still matches its transaction after both are
rewritten.

```
banking:backfill-descriptions [--user=email] [--dry-run] [-v]
```

## QA

Ran the formatter over **all 2755 distinct tagged descriptions in
production**: 0 no-ops, 0 leftover tags/dates/markers, 0 degenerate
output.

Ran the command against a database seeded with production-shaped rows
and rules:

```
===== DRY RUN (-v) =====
DRY RUN — no changes will be saved.
  /TXT/D|SumUp *GELATERIA SALV            →  SumUp *GELATERIA SALV
  /TXT/H|TRANSF NOMI /AIGUA DE RIGAT, S   →  TRANSF NOMI /AIGUA DE RIGAT, S
  /TXT/EL CLANDESTI 27/07/26|20260803     →  EL CLANDESTI
  /TXT/CONDIS SANT JUST DESV07/07/26|20260714  →  CONDIS SANT JUST DESV
  /TXT/RECIBO MES TARJETA|20260806        →  RECIBO MES TARJETA
  /TXT/D|#RECOBRO RECIBO VISA             →  RECOBRO RECIBO VISA
  /TXT/OPENAI *CHATGPT SUBSCR MP          →  OPENAI *CHATGPT SUBSCR MP
  rule …080: {"in":["RECIBO VISA CLASICA",{"var":"description"}]}
8 transaction(s) and 2 automation rule(s) would be reformatted.

===== SECOND RUN =====
0 transaction(s) and 0 automation rule(s) reformatted.     ← idempotent
```

The raw text is kept in `original_description`; an already-stored
original is never overwritten; untagged descriptions and rules are left
alone; transactions the server cannot read (`description_iv`) are
skipped.

## Not in this PR

**Already-categorized transactions keep their category.** The backfill
fixes the text, not the past AI decisions — 4005 of the affected rows
already have a category (1024 of them AI-sourced), and
`ai:categorize-backfill` only picks up uncategorized ones.
Rule-categorized rows are unaffected because the rules are migrated. If
we want the AI ones re-run, that's a separate deliberate step: clear
`category_source = 'ai'` on the affected rows, then run the existing
backfill command.

## Rollout

```bash
php artisan banking:backfill-descriptions --dry-run -v     # inspect
php artisan banking:backfill-descriptions                  # apply
```
2026-08-09 18:11:05 +02:00
..
AgentDatabaseCommandTest.php feat(console): add agent:db command for querying local and prod DB (#522) 2026-06-12 18:35:14 +02:00
BackfillTransactionDescriptionsCommandTest.php fix(banking): strip the ISO 20022 remittance tag from transaction descriptions (#744) 2026-08-09 18:11:05 +02:00
CheckBankLogosCommandTest.php Add weekly bank logo audit command (#211) 2026-03-06 11:05:13 +00:00
DeleteAiConsentCommandTest.php feat(ai): add command to delete a user's AI consent by email (#729) 2026-07-23 08:35:28 +00:00
DeleteEncryptedDataAccountsCommandTest.php fix(encryption): never email or delete users who are still being billed (#656) 2026-07-07 16:04:27 +00:00
DeleteManualAccountDataCommandTest.php feat(commands): delete transactions and balances of a user's non-connected accounts (#728) 2026-07-23 08:26:11 +00:00
FeatureEnableCommandTest.php refactor(ai): remove AiConsentSettings feature flag (#619) 2026-07-01 09:47:55 +02:00
NotifyEncryptedDataRemovalCommandTest.php feat(encryption): report count of users still holding encrypted data (#687) 2026-07-17 09:29:33 +00:00
ResetDemoAccountCommandTest.php fix: skip demo reset when demo account is disabled (#626) 2026-07-03 07:27:43 +00:00
ResetUserCategoriesCommandTest.php fix(categories): expose cashflow setting on create (#448) 2026-05-29 11:05:04 +02:00
SendPaywallFollowUpEmailsCommandTest.php feat(drip): email users stuck on the paywall a day after onboarding (#562) 2026-06-19 14:11:12 +00:00
SendSubscriptionCancelledEmailCommandTest.php Add subscription cancellation email with manual trigger command (#47) 2025-12-30 20:32:44 +01:00
SendUpdateEmailCommandTest.php Support soft-deleted users with reusable emails (#316) 2026-04-22 11:41:41 +01:00
SetBankLogoCommandTest.php fix(banks:set-logo): add JPEG support test coverage and prompt for missing arguments (#214) 2026-03-07 16:33:54 +00:00
SetupMainUserCommandTest.php Command: Setup Main User 2025-11-15 22:16:41 +01:00