https://github.com/drake69/spendify
๐ฆ Personal finance ledger โ aggregates bank statements (CSV/XLSX) into a single ledger with hybrid deterministic + LLM categorization. Offline-first, privacy-safe.
https://github.com/drake69/spendify
bank-statements budgeting finance llm ollama personal-finance python self-hosted sqlite streamlit
Last synced: 2 months ago
JSON representation
๐ฆ Personal finance ledger โ aggregates bank statements (CSV/XLSX) into a single ledger with hybrid deterministic + LLM categorization. Offline-first, privacy-safe.
- Host: GitHub
- URL: https://github.com/drake69/spendify
- Owner: drake69
- License: other
- Created: 2026-01-22T11:08:02.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-04-08T13:15:39.000Z (3 months ago)
- Last Synced: 2026-04-08T15:06:37.164Z (3 months ago)
- Topics: bank-statements, budgeting, finance, llm, ollama, personal-finance, python, self-hosted, sqlite, streamlit
- Language: Python
- Homepage: https://drake69.github.io/spendify
- Size: 2.7 MB
- Stars: 2
- Watchers: 1
- Forks: 1
- Open Issues: 28
-
Metadata Files:
- Readme: README.it.md
- License: LICENSE
- Support: support/core_logic.py
Awesome Lists containing this project
README
# Spendify v3.0
[](https://github.com/drake69/spendify/actions/workflows/ci.yml)
[](https://codecov.io/gh/drake69/spendify)
[](https://www.python.org/downloads/)
[](LICENSE)
[](https://github.com/astral-sh/uv)
[](https://streamlit.io)
[](https://github.com/drake69/spendify/issues)
[](https://github.com/drake69/spendify/commits/main)
> ๐ฌ๐ง [Read in English](README.md)
Registro finanziario personale unificato con pipeline ibrida deterministica + LLM.
Aggrega estratti conto eterogenei (conti correnti, carte di credito, carte di debito, conti deposito, prepagate) in un unico ledger cronologico, eliminando il double-counting da addebiti carta periodici e da giroconti interni. Il processing avviene in modalitร **offline-first**; i backend LLM remoti sono supportati come opt-in con sanitizzazione PII obbligatoria.
---
## Indice
- [Caratteristiche principali](#caratteristiche-principali)
- [Architettura](#architettura)
- [Struttura del progetto](#struttura-del-progetto)
- [Installazione](#installazione)
- [Configurazione](#configurazione)
- [Avvio](#avvio)
- [Tassonomia](#tassonomia)
- [Motore delle regole](#motore-delle-regole)
- [Giroconti](#giroconti)
- [Test](#test)
- [Decisioni di design](#decisioni-di-design)
---
## Caratteristiche principali
| Funzionalitร | Dettaglio |
|---|---|
| **Classificazione automatica** | Rileva tipo di documento (conto corrente, carta, prepagata, deposito) senza configurazione preventiva |
| **Normalizzazione deterministica** | Encoding detection, delimiter detection, header detection, importi in `Decimal` (mai `float`) |
| **Correzione segno carta** | Flag `invert_sign` in `DocumentSchema`: quando un file carta salva le spese come valori positivi, vengono negati automaticamente |
| **Idempotenza SHA-256** | Re-importare lo stesso file produce esattamente lo stesso insieme di righe |
| **Riconciliazione cartaโc/c (RF-03)** | Algoritmo a 3 fasi che elimina il double-counting da addebiti aggregati mensili |
| **Rilevamento giroconti (RF-04)** | Matching simbolico importo+finestra temporale; esclusione o neutralizzazione configurabile |
| **Categorizzazione a cascata (RF-05)** | Regole utente โ regex statiche โ LLM strutturato โ fallback "Altro" |
| **Motore regole con applicazione retroattiva** | Le regole deterministiche vengono applicate a tutte le transazioni esistenti al momento del salvataggio, non solo alle future importazioni |
| **Sottocategoria come fonte di veritร ** | La sottocategoria รจ la chiave primaria: se LLM o regola assegna una sottocategoria presente in tassonomia, la categoria genitore viene risolta automaticamente |
| **Wizard di onboarding guidato** | Wizard in 4 step al primo avvio: selezione lingua (rilevata dal browser), nomi titolari, conti bancari, conferma. Scrittura atomica: il DB viene popolato solo al clic su "Inizia!". Saltato automaticamente se esiste giร una tassonomia (installazioni esistenti). |
| **Tassonomia multi-lingua** | Template built-in in 5 lingue (๐ฎ๐น ๐ฌ๐ง ๐ซ๐ท ๐ฉ๐ช ๐ช๐ธ). Salvati nella tabella `taxonomy_default` del DB (nessun file YAML richiesto). Lingua scelta all'onboarding; reimpostabile da Impostazioni in qualsiasi momento. |
| **Tassonomia a 2 livelli nel DB** | 15 categorie di spesa + 7 di entrata; gestita dalla pagina Tassonomia (DB-backed, nessun restart richiesto) |
| **Backend LLM multi-provider** | Ollama (locale, default), OpenAI, Claude โ interfaccia astratta comune, nessun LangChain |
| **Config LLM nell'UI** | Backend, modello e chiavi API configurabili dalla pagina Impostazioni senza toccare `.env` |
| **PII sanitization (RF-10)** | IBAN, PAN, CF, nomi del titolare redatti prima di qualsiasi chiamata remota |
| **Circuit breaker** | Fallback automatico su Ollama locale; quarantena (`to_review=True`) se tutti i backend falliscono |
| **Contesti di vita** | Dimensione ortogonale configurabile dall'utente (es. Quotidianitร / Lavoro / Vacanza) assegnabile a ogni transazione; suggerimenti automatici basati su similaritร Jaccard con transazioni precedenti |
| **Re-run LLM su fallimenti** | Pulsante nella pagina Review che rielabora solo le transazioni in cui l'LLM aveva fallito (`description == raw_description`) |
| **Rilevamento giroconti cross-account** | Pulsante nella pagina Review che riesegue `detect_internal_transfers` globalmente su tutte le transazioni, intercettando le coppie non trovate in fase di import |
| **Permutazioni nome titolare** | Tutte le permutazioni dei token del nome del titolare vengono verificate per il rilevamento giroconti, evitando i falsi negativi quando l'ordine varia tra i file |
| **Service layer + CI coupling gate** | Tutte le pagine UI importano solo da `services.*`, mai da `core.*` o `db.*` direttamente. `tools/coupling_check.py --strict` applica il vincolo in CI; le violazioni bloccano la PR. |
| **Persistenza SQLAlchemy** | 11 tabelle ORM; CRUD idempotente; migrazioni automatiche all'avvio |
| **Progresso import cross-session** | Stato del job di importazione salvato nel DB; tutte le sessioni browser vedono il progresso in tempo reale |
| **Export report** | HTML standalone (Plotly), CSV, XLSX |
| **UI Streamlit 9 pagine** | Import โ Ledger โ Modifiche massive โ Analytics โ Review โ Regole โ Tassonomia โ Impostazioni โ Check List |
| **Check List mensile** | Tabella pivot mese ร conto con conteggio transazioni; evidenzia i mesi mancanti a colpo d'occhio |
---
## Architettura
```
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ app.py (Streamlit) โ
โ [onboarding gate] โ sidebar โ upload โ ledger โ bulk-edit โ analytics โ
โ review โ rules โ taxonomy โ settings โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ services.* (facade layer)
core/orchestrator.py
ProcessingConfig ยท process_file()
โ
โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโ
โ โ โ
Flow 1 (template) Flow 2 (schema-on-read)
DocumentSchema classifier.py โ LLM โ DocumentSchema
giร noto (campione sanitizzato) invert_sign detection
โ
normalizer.py sanitizer.py llm_backends.py
โโ encoding detect โโ IBAN/PAN/CF โโ OllamaBackend
โโ parse_amount() โโ owner names โโ OpenAIBackend
โโ SHA-256 tx_id โโ assert_sani.. โโ ClaudeBackend
โโ invert_sign BackendFactory
โโ RF-03 reconcile call_with_fallback()
โโ RF-04 transfers
โ
categorizer.py โโโ TaxonomyConfig (caricato dal DB)
Step 0: regole utente (risoluzione sottocategoria โ categoria)
Step 1: regex statiche
Step 2: stub ML
Step 3: LLM structured output (enum sottocategorie vincolato)
Step 4: fallback "Altro"
โ
db/repository.py (SQLAlchemy, idempotente)
โโ Transaction ยท ImportBatch ยท DocumentSchemaModel
ReconciliationLink ยท InternalTransferLink ยท CategoryRule
UserSettings ยท ImportJob ยท Account
TaxonomyCategory ยท TaxonomySubcategory ยท TaxonomyDefault
โ
reports/generator.py
โโ HTML (Jinja2+Plotly) ยท CSV ยท XLSX
```
### Flow 1 vs Flow 2
| | Flow 1 | Flow 2 |
|---|---|---|
| **Attivazione** | `DocumentSchema` giร in DB per quel fingerprint colonne | Prima importazione di un nuovo formato |
| **Schema** | Recuperato da DB, applicato direttamente | LLM inferisce lo schema da un campione anonimizzato |
| **Promozione** | โ | Il template Flow 2 approvato viene salvato e diventa Flow 1 |
| **Costo LLM** | Zero (solo categorizzazione) | Una chiamata per classificazione + una per categorizzazione batch |
---
## Struttura del progetto
```
spendify/
โโโ app.py # Entry point Streamlit โ onboarding gate + 9 pagine
โโโ .env.example # Template variabili d'ambiente
โโโ pyproject.toml # Dipendenze (uv / pip)
โ
โโโ core/
โ โโโ models.py # Enum: DocumentType, TransactionType, GirocontoMode โฆ
โ โโโ schemas.py # DocumentSchema (Pydantic) + invert_sign + llm_json_schema()
โ โโโ llm_backends.py # LLMBackend ABC ยท Ollama ยท OpenAI ยท Claude ยท BackendFactory
โ โโโ sanitizer.py # PII redaction (RF-10)
โ โโโ normalizer.py # Encoding, parse_amount (Decimal), SHA-256, RF-03, RF-04
โ โโโ classifier.py # Flow 2: inferenza DocumentSchema via LLM
โ โโโ categorizer.py # Cascata 4-step + TaxonomyConfig (find_category_for_subcategory)
โ โโโ orchestrator.py # Pipeline principale: ProcessingConfig ยท process_file()
โ
โโโ db/
โ โโโ models.py # ORM SQLAlchemy (11 tabelle) + migrazioni automatiche
โ โโโ repository.py # CRUD idempotente ยท persist_import_result() ยท CRUD tassonomia
โ โ # bulk_set_giroconto_by_description()
โ โ # get_transactions_by_rule_pattern()
โ โ # seed_user_taxonomy_from_default()
โ โโโ taxonomy_defaults.py # Template tassonomia built-in (5 lingue: it/en/fr/de/es)
โ # TAXONOMY_DEFAULTS ยท SUPPORTED_LANGUAGES
โ
โโโ services/ # Facade layer โ la UI importa solo da qui, mai da core.* o db.*
โ โโโ settings_service.py # SettingsService: CRUD impostazioni + tassonomia + conti + onboarding
โ โโโ import_service.py # ImportService: analisi file, pipeline, cache schema + re-export tipi
โ
โโโ reports/
โ โโโ generator.py # HTML (Jinja2+Plotly) ยท CSV ยท XLSX
โ โโโ template_report.html.j2
โ
โโโ ui/
โ โโโ onboarding_page.py # Wizard primo avvio (4 step: lingua โ titolari โ conti โ conferma)
โ โโโ sidebar.py # Pulsanti navigazione (9 pagine) + modalitร giroconto
โ โโโ upload_page.py # Import multi-file + progress bar cross-session
โ โโโ registry_page.py # Ledger filtrabile + selezione al click + bulk giroconto
โ โโโ analysis_page.py # 7 grafici Plotly: barre mensili, saldo cumulativo,
โ โ # pie+treemap spese, drill-down categoria, pie+treemap entrate,
โ โ # top-10 descrizioni, stacked per conto + export HTML
โ โโโ review_page.py # Correzione categoria + toggle giroconto + salvataggio regola
โ โโโ bulk_edit_page.py # Operazioni massive: categoria/contesto/giroconto + eliminazione da filtro
โ โโโ rules_page.py # CRUD completo regole + "Esegui tutte le regole" bulk re-categorizzazione
โ โโโ taxonomy_page.py # CRUD DB-backed per categorie e sottocategorie
โ โโโ settings_page.py # Locale, lingua, config LLM, expander reset tassonomia
โ โโโ checklist_page.py # Pivot mese ร conto: checklist presenza transazioni
โ
โโโ tools/
โ โโโ coupling_check.py # Validatore accoppiamento architetturale (UI โ services.* only)
โ โ # --strict: applica baseline, usato in CI
โ โโโ coupling_baseline.json # Soglie max violazioni per file (attualmente tutte zero)
โ
โโโ prompts/
โ โโโ classifier.json # Prompt Flow 2 (hint invert_sign per file carta)
โ โโโ categorizer.json # Prompt categorizzazione transazioni
โ
โโโ tests/
โ โโโ test_normalizer.py # Test deterministici (parse_amount, SHA-256 โฆ)
โ โโโ test_backends.py # Factory backend, validazione, mock Ollama
โ โโโ test_categorizer.py # Regole statiche, cascata, risoluzione tassonomia
โ โโโ test_repository_rules.py # Upsert regole, pattern matching, toggle giroconto, bulk ops
โ โโโ test_taxonomy_onboarding.py # Template multi-lingua + servizi onboarding (27 test)
โ
โโโ support/
โโโ formatting.py # format_amount_display, format_date_display, format_raw_amount_display
โโโ logging.py
```
---
## Installazione
### โก Installazione rapida (Docker โ niente git clone)
L'unico prerequisito รจ **[Docker Desktop](https://www.docker.com/products/docker-desktop/)** installato e avviato.
**Mac / Linux:**
```bash
curl -fsSL https://raw.githubusercontent.com/drake69/spendify/main/installer/install.sh | bash
```
**Windows (PowerShell):**
```powershell
irm https://raw.githubusercontent.com/drake69/spendify/main/installer/install.ps1 | iex
```
Lo script scarica l'immagine pre-compilata da GitHub Container Registry, avvia il container e apre il browser su **http://localhost:8501** automaticamente.
> **AI locale opzionale:** l'installer chiede se aggiungere Ollama + `gemma3:12b` (scaricato automaticamente, ~8 GB). Compatibile con Apple Silicon (arm64) e amd64.
> **Aggiornamento all'ultima versione:**
> ```bash
> docker compose --project-directory ~/spendify pull && docker compose --project-directory ~/spendify up -d
> ```
> **Disinstallazione:** `curl -fsSL https://raw.githubusercontent.com/drake69/spendify/main/installer/uninstall.sh | bash`
---
### Installazione developer (nativa, consigliata su Mac)
> Setup completo, convenzioni di codice, sistema di prioritร e flusso PR โ **[CONTRIBUTING.md](CONTRIBUTING.md)**
### Prerequisiti
- **Python 3.13+**
- **[uv](https://github.com/astral-sh/uv)** (gestore pacchetti consigliato)
- **[Ollama](https://ollama.com)** per il backend LLM locale (default)
### 1. Clona il repository
```bash
git clone https://github.com/drake69/spendify.git
cd spendify
```
### 2. Installa le dipendenze
```bash
uv sync
```
### 3. Configura le variabili d'ambiente
```bash
cp .env.example .env
# Nessuna modifica necessaria per un'installazione locale standard โ percorsi giร impostati
```
### 4. Scarica il modello LLM locale (opzionale)
```bash
ollama pull gemma3:12b # ~8 GB โ salta se hai intenzione di usare OpenAI/Anthropic
```
> Mantieni Ollama in esecuzione (`ollama serve`) durante l'uso dell'app. Backend LLM, modello e API key si configurano dalla pagina **โ๏ธ Impostazioni** โ non nel `.env`.
---
## Configurazione
Il file `.env` contiene solo parametri infrastrutturali. Tutto il resto โ backend LLM, chiavi API, modello, nomi titolari per PII redaction, formato data, lingua โ รจ configurabile dalla pagina **โ๏ธ Impostazioni** e persiste nel DB:
```dotenv
# URI database โ lascia invariato per uso locale; sovrascritto da docker-compose per Docker
SPENDIFY_DB=sqlite:///ledger.db
```
> **Nient'altro appartiene al `.env`.** Backend LLM, URL Ollama, nome modello, chiavi API OpenAI/Anthropic e nomi titolari sono tutti salvati nella tabella `user_settings` e modificabili live dall'UI senza riavviare l'app.
### Modalitร giroconto
Configurabile dalla sidebar dell'app:
| Modalitร | Comportamento |
|---|---|
| `neutral` | I giroconti restano nel ledger come `internal_out` / `internal_in` (default) |
| `exclude` | I giroconti vengono rimossi dal registro (saldo netto non influenzato) |
### Privacy e backend remoti
```
[LOCAL โ default] Ollama locale: nessun dato esce dal processo.
Nessuna sanitizzazione richiesta.
[REMOTE โ opt-in] OpenAI / Claude: PII sanitization OBBLIGATORIA.
IBAN โ | PAN โ
CF โ | owner โ
Chiamata bloccata se assert_sanitized() fallisce.
```
---
## Avvio
```bash
# Con uv
uv run streamlit run app.py
# Oppure
streamlit run app.py
```
Al **primo avvio** appare automaticamente il wizard di onboarding (4 step: lingua, nomi titolari, conti, conferma). Le installazioni esistenti con dati giร nella tassonomia vengono rilevate automaticamente e saltano il wizard.
L'app si apre su `http://localhost:8501` con 9 pagine:
| Pagina | Descrizione |
|---|---|
| **๐ฅ Import** | Carica uno o piรน file (CSV / XLSX). Progresso live visibile da tutte le sessioni browser. Riepilogo: transazioni, riconciliazioni, transfer link, flow usato (1/2). |
| **๐ Ledger** | Tabella filtrabile per data, tipo, descrizione, categoria, contesto, flag revisione. Click su una riga per selezionarla istantaneamente. Colonne Entrata/Uscita separate e allineate a destra. Filtro contesto + pannello assegnazione con suggerimenti Jaccard. Toggle giroconto con bulk-apply. Download CSV/XLSX. |
| **โ๏ธ Modifiche massive** | Operazioni in blocco su transazione di riferimento: toggle giroconto, assegnazione contesto (con similaritร Jaccard), correzione categoria + salvataggio regola. Eliminazione massiva tramite filtri combinati (data, conto, tipo, descrizione, categoria) con anteprima e conferma `ELIMINA` obbligatoria. |
| **๐ Analytics** | 7 grafici Plotly interattivi: barre mensili entrate/uscite, saldo cumulativo, pie+treemap spese per categoria, drill-down interattivo categoriaโsottocategoria con trend mensile, pie+treemap entrate, top-10 descrizioni, stacked per conto. Export HTML. |
| **๐ Review** | Transazioni con `to_review=True`. Toggle giroconto (con bulk-apply). Correzione categoria/sottocategoria + salvataggio opzionale come regola permanente applicata immediatamente. Pulsante "Re-run LLM" per transazioni non pulite. Pulsante "Riesegui giroconti cross-account". |
| **๐ Regole** | CRUD completo regole di categorizzazione. Modifica/elimina regole + ricalcolo bulk delle transazioni giร categorizzate. Pulsante "โถ๏ธ Esegui tutte le regole" applica tutte le regole a ogni transazione del ledger in un colpo. |
| **๐๏ธ Tassonomia** | CRUD DB-backed per categorie e sottocategorie (spese e entrate). Le modifiche hanno effetto immediato senza restart. |
| **โ๏ธ Impostazioni** | Formato data, separatori importo, lingua descrizioni, contesti di vita, lista conti bancari, backend LLM (modello + chiavi API). Tutto persistito nel DB. |
| **โ
Check List** | Tabella pivot mese ร conto. Mese corrente in cima, ordine decrescente. Celle: numero tx o **โ** se assenti. Colorazione per volume. Filtri: selezione conti, ultimi N mesi, nascondi mesi vuoti. Export CSV. |
---
## Tassonomia
La tassonomia รจ memorizzata nel database (tabelle `taxonomy_category` / `taxonomy_subcategory`) e gestita dalla pagina **๐๏ธ Tassonomia**.
### Template multi-lingua built-in
La tabella `taxonomy_default` contiene template immutabili in 5 lingue โ Italiano, Inglese, Francese, Tedesco, Spagnolo. Vengono popolati da `db/taxonomy_defaults.py` al primo avvio (nessun file YAML). Durante l'onboarding l'utente seleziona la lingua e il template viene copiato nella tassonomia utente.
Per reimpostare la tassonomia su una lingua diversa in qualsiasi momento: **โ๏ธ Impostazioni โ ๐ Reset tassonomia**.
### Tassonomia di default (Italiano)
**Categorie di spesa (15):** Casa ยท Alimentari ยท Ristorazione ยท Trasporti ยท Salute ยท Istruzione ยท Abbigliamento ยท Comunicazioni ยท Svago e tempo libero ยท Animali domestici ยท Finanza e assicurazioni ยท Cura personale ยท Tasse e tributi ยท Regali e donazioni ยท Altro
**Categorie di entrata (7):** Lavoro dipendente ยท Lavoro autonomo ยท Rendite finanziarie ยท Rendite immobiliari ยท Trasferimenti e rimborsi ยท Prestazioni sociali ยท Altro entrate
**La sottocategoria รจ la fonte di veritร :** se LLM o una regola assegnano una sottocategoria presente in tassonomia, la categoria genitore corretta viene risolta automaticamente โ i due livelli sono sempre consistenti nel DB.
---
## Motore delle regole
Le regole di categorizzazione sono memorizzate nella tabella `category_rule` e applicate in piรน punti del ciclo di vita.
### Tipi di matching
| Tipo | Comportamento |
|---|---|
| `contains` | Il pattern appare ovunque nella descrizione (case-insensitive) |
| `exact` | La descrizione corrisponde esattamente al pattern (case-insensitive) |
| `regex` | Regex Python completa confrontata con la descrizione |
`get_transactions_by_rule_pattern` ricerca **tutte** le transazioni indipendentemente da come erano state categorizzate (LLM, regola o correzione manuale). Salvare una nuova regola corregge correttamente anche le transazioni giร categorizzate dall'LLM.
### Prioritร
Quando piรน regole corrispondono alla stessa transazione vince quella con il valore di `priority` piรน alto. La prioritร di default รจ 10; รจ possibile assegnare qualsiasi intero.
### Semantica upsert
Creare una regola con la stessa coppia `(pattern, match_type)` di una regola esistente la **aggiorna** sul posto (categoria, sottocategoria, prioritร ) anzichรฉ creare un duplicato.
### Applicazione retroattiva
Salvare una regola dalle pagine **Ledger** o **Review** la applica immediatamente a tutte le transazioni esistenti che corrispondono al pattern, non solo alle future importazioni. Il messaggio di conferma indica quante transazioni sono state aggiornate. Lo stesso comportamento รจ disponibile dalla pagina **Regole** tramite l'opzione di ricalcolo bulk su singola regola.
Inoltre, il pulsante **โถ๏ธ Esegui tutte le regole** nella pagina **Regole** applica tutte le regole a ogni transazione del ledger in un colpo solo (non limitato a `to_review=True`). Utile dopo aver creato piรน regole contemporaneamente o dopo aver importato dati storici.
---
## Giroconti
Un *giroconto* รจ un movimento interno tra conti di propria titolaritร (es. bonifico da conto corrente a conto deposito, ricarica di una prepagata). Includere entrambi i lati nel saldo causerebbe double-counting.
### Tipi di transazione
| `tx_type` | Significato |
|---|---|
| `internal_out` | Lato uscente del giroconto (importo negativo) |
| `internal_in` | Lato entrante del giroconto (importo positivo) |
Entrambi i tipi sono esclusi dal saldo netto, dalle entrate e dalle uscite.
### Rilevamento automatico (RF-04)
La pipeline tenta di abbinare i giroconti automaticamente durante l'importazione con tre passaggi:
1. **Regex keyword** โ la descrizione corrisponde a un pattern configurato (es. "Giroconto", "Bonifico tra i miei conti") โ alta confidenza
2. **Matching importo + data** โ stesso importo assoluto entro ยฑ3 giorni, su `account_label` diversi โ confidenza media/alta
3. **Permutazioni nome titolare** โ la descrizione contiene qualsiasi permutazione dei token del nome del titolare โ alta confidenza (intercetta sia "Corsaro Luigi Gerotti Elena" che "Luigi Corsaro Elena Gerotti")
### Riesecuzione cross-account
Quando le due transazioni di un giroconto appartengono a file importati in momenti diversi, il primo import non puรฒ trovare la coppia. Usa il pulsante **"๐ Riesegui rilevamento giroconti"** nella pagina **๐ Review** per rieseguire il rilevamento globalmente su tutte le transazioni non-giroconto.
### Toggle manuale
Dalle pagine **Ledger** o **Review** รจ possibile contrassegnare manualmente qualsiasi transazione come giroconto (o ripristinarla):
- **Toggle singolo** โ cambia il `tx_type` della transazione selezionata (`expense` โ `internal_out`, `income` โ `internal_in`).
- **Bulk apply** โ se altre transazioni condividono la stessa descrizione, una checkbox (default: abilitata) consente di applicare la stessa modifica a tutte con un solo click. Il numero di transazioni coinvolte รจ visibile prima di confermare.
`bulk_set_giroconto_by_description` in `db/repository.py` implementa l'operazione bulk: aggiorna tutte le transazioni con la descrizione indicata eccetto quella giร modificata, e restituisce il numero di righe cambiate.
---
## Contesti di vita
I contesti di vita sono una dimensione di classificazione ortogonale alla tassonomia delle categorie. Mentre la categoria risponde *cosa รจ stato acquistato*, il contesto risponde *per quale area della vita*.
### Design
| Aspetto | Dettaglio |
|---|---|
| **Storage** | Colonna `context VARCHAR(64)` nullable sulla tabella `Transaction` |
| **Ortogonalitร ** | Indipendente da categoria/sottocategoria โ qualsiasi combinazione รจ valida |
| **Configurabile** | Aggiunta, rinomina e rimozione contesti dalla pagina **โ๏ธ Impostazioni** (salvati come JSON in `user_settings`) |
| **Contesti default** | Quotidianitร ยท Lavoro ยท Vacanza |
### Assegnazione
Dalla pagina **๐ Ledger**, seleziona una transazione e apri il pannello espandibile "๐ Assegna contesto":
1. Scegli un contesto dal menu a discesa (o cancella quello esistente)
2. Attiva opzionalmente **"Applica anche a transazioni simili"** โ la similaritร Jaccard a livello di token (soglia 0.35) trova transazioni con descrizione semanticamente vicina e pre-assegna lo stesso contesto
3. Clicca **Applica**
### Filtro
La barra filtri del registro include un selettore contesto: *tutti*, i singoli valori configurati, o *โ nessuno โ* (transazioni senza contesto assegnato).
---
## Test
```bash
# Tutti i test (nessun mock LLM richiesto)
uv run python -m pytest tests/ -v
# Con coverage
uv run python -m pytest tests/ --cov=core --cov=db --cov-report=term-missing
```
### File di test
| File | Copertura |
|---|---|
| `test_normalizer.py` | `parse_amount`, dedup SHA-256, encoding detection |
| `test_backends.py` | Factory backend, validazione, mock Ollama |
| `test_categorizer.py` | Regole statiche, cascata 4-step, risoluzione tassonomia |
| `test_repository_rules.py` | Upsert regole, `get_transactions_by_rule_pattern` (tutti i tipi + regressione LLM-sourced), `apply_rules_to_review_transactions`, `toggle_transaction_giroconto`, `bulk_set_giroconto_by_description` |
| `test_taxonomy_onboarding.py` | Struttura `TAXONOMY_DEFAULTS` (5 lingue), migrazione `taxonomy_default` (seeding + idempotenza), metodi onboarding di `SettingsService`, auto-skip per utenti esistenti |
Tutti i test usano un database SQLite in-memory โ nessun I/O su file, nessun servizio esterno richiesto.
---
## Decisioni di design
### `Decimal` โ mai `float`
Tutti gli importi sono `decimal.Decimal`. I float IEEE 754 introducono errori di arrotondamento che falsano saldi e riconciliazioni.
### Idempotenza SHA-256
Ogni transazione ha un `id` di 24 caratteri (SHA-256 troncato) calcolato deterministicamente da `(source_file, date, amount, description)`. Re-importare lo stesso file non genera duplicati.
### Correzione segno carta (`invert_sign`)
Gli estratti conto italiani per carte di credito/debito esportano spesso gli acquisti come valori positivi. Il flag `DocumentSchema.invert_sign`, impostato dall'LLM durante la classificazione Flow 2, istruisce il normalizzatore a negare tutti gli importi โ le spese diventano negative e i rimborsi positivi con un'unica operazione simmetrica.
#### Algoritmo di rilevamento in due passi
Il classificatore decide il valore di `invert_sign` con un algoritmo in due passi. **Lo Step 0 ha la prioritร massima: se si attiva, lo Step 1 viene saltato completamente.** Lo Step 1 รจ consultato solo quando lo Step 0 non riesce a dare una risposta definitiva.
**Step 0 โ Sinonimi del nome colonna (prioritร massima)**
Il nome della colonna importo viene confrontato con tre gruppi di sinonimi:
| Gruppo | Esempi di nomi | Decisione |
|---|---|---|
| **Sinonimi di uscita** | Uscita, Uscite, Addebito, Addebiti, Pagamento, Spesa, Dare, Importo addebitato | `invert_sign = true` (spese salvate come positivi โ negarle) |
| **Sinonimi di entrata** | Entrata, Entrate, Accredito, Accrediti, Avere, Credito, Importo accreditato | `invert_sign = false` (entrate giร positive โ nessuna modifica) |
| **Nomi neutri** | Importo, Amount, Valore, Totale | Nessuna decisione โ si procede allo Step 1 |
Il matching รจ case-insensitive e parziale (es. "Addebiti carta" corrisponde a "Addebito"). La regola dei sinonimi di uscita si applica solo ai doc_type carta; conti correnti e depositi mantengono sempre `invert_sign = false` indipendentemente dal nome della colonna.
**Step 1 โ Analisi della distribuzione dei segni (solo nomi neutri)**
Quando lo Step 0 trova un nome neutro e non puรฒ classificare per nome, il classificatore conta i valori positivi e negativi nel campione e calcola `positive_ratio` e `negative_ratio`:
- File carta, maggioranza positivi (> 60 %): le spese sono salvate come positivi (convenzione AMEX / tipici export italiani) โ `invert_sign = true`
- File carta, maggioranza negativi (> 60 %): le spese hanno giร il segno corretto โ `invert_sign = false`
- Split circa 50/50: si analizzano le descrizioni (nomi di esercenti con importi positivi โ `invert_sign = true`; "bonifico ricevuto" con importo positivo โ `invert_sign = false`)
- Conto corrente / deposito: sempre `invert_sign = false`, indipendentemente dalla distribuzione
#### Campi diagnostici
Ogni `DocumentSchema` prodotto dal Flow 2 include quattro campi diagnostici per audit e debug:
| Campo | Tipo | Contenuto |
|---|---|---|
| `positive_ratio` | `float \| null` | Frazione di valori > 0 nella colonna importo nel campione |
| `negative_ratio` | `float \| null` | Frazione di valori < 0 nella colonna importo nel campione |
| `semantic_evidence` | `list[str]` | 2โ4 frasi brevi dell'LLM che spiegano la decisione |
| `normalization_case_id` | `str \| null` | C1 = conto corrente signed_single ยท C2 = carta invertita ยท C3 = carta giร negativa ยท C4 = colonne Dare/Avere ยท C5 = ambiguo |
Questi campi sono persistiti nella tabella DB `document_schema` e visibili nel riepilogo dello schema Flow 2 nell'UI.
### Sottocategoria come chiave primaria
Il categorizzatore tratta la sottocategoria come autoritativa. `TaxonomyConfig.find_category_for_subcategory()` risolve la categoria genitore da qualsiasi nome di sottocategoria valido. LLM e regole possono specificare il livello piรน granulare e la gerarchia รจ sempre consistente nel DB.
### Tassonomia nel DB
La tassonomia a 2 livelli (categorie + sottocategorie) risiede in due tabelle DB (`taxonomy_category`, `taxonomy_subcategory`). Al primo avvio il wizard di onboarding copia il template della lingua scelta dalla tabella immutabile `taxonomy_default` nella tassonomia utente. Nessun file YAML. Le modifiche successive sono gestite interamente dall'UI โ nessun restart richiesto.
### PII sanitization come precondizione
`assert_sanitized()` รจ chiamata in `call_with_fallback()` prima di qualsiasi richiesta a backend remoto. Se il testo contiene pattern IBAN/PAN/CF rilevabili, la chiamata viene rifiutata โ non degradata silenziosamente.
### Circuit breaker e quarantena
`call_with_fallback(primary, ...)` prova il backend primario, poi Ollama locale come fallback. Se entrambi falliscono, la transazione riceve `to_review=True` e viene messa in coda senza bloccare il resto del batch.
### Nessun LangChain
I backend LLM usano direttamente `openai` SDK, `anthropic` SDK e `requests` (per Ollama). Nessuna dipendenza da framework di orchestrazione LLM.
### RF-03: algoritmo a 3 fasi
La riconciliazione cartaโconto corrente usa: (1) finestra temporale ยฑ45 giorni, (2) sliding window contigua (gap โค 5 giorni, O(nยฒ)), (3) subset sum al boundary (k=10 tx, ~10โถ operazioni).
---
## Dipendenze principali
| Pacchetto | Versione | Scopo |
|---|---|---|
| `streamlit` | โฅ 1.35 | UI |
| `pandas` | โฅ 2.2 | Elaborazione dati |
| `sqlalchemy` | โฅ 2.0 | ORM / persistenza |
| `pydantic` | โฅ 2.0 | Validazione schemi |
| `openai` | โฅ 1.30 | Backend OpenAI |
| `anthropic` | โฅ 0.28 | Backend Claude |
| `requests` | โฅ 2.31 | Backend Ollama |
| `chardet` | โฅ 5.0 | Encoding detection |
| `plotly` | โฅ 5.20 | Grafici |
| `jinja2` | โฅ 3.1 | Template report HTML |
| `pyyaml` | โฅ 6.0 | Parsing seed taxonomy.yaml |
| `pytest` | โฅ 8.0 | Test |
---
*Tutti i dati sono salvati localmente nel database SQLite (`ledger.db`). Nessuna informazione finanziaria viene trasmessa a servizi esterni salvo esplicita configurazione del backend remoto e sanitizzazione PII obbligatoria.*