An open API service indexing awesome lists of open source software.

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.

Awesome Lists containing this project

README

          

# Spendify v3.0

[![CI](https://github.com/drake69/spendify/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/drake69/spendify/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/drake69/spendify/graph/badge.svg)](https://codecov.io/gh/drake69/spendify)
[![Python 3.13](https://img.shields.io/badge/python-3.13-blue?logo=python&logoColor=white)](https://www.python.org/downloads/)
[![License: PolyForm NC](https://img.shields.io/badge/license-PolyForm%20Noncommercial-orange)](LICENSE)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
[![Streamlit](https://img.shields.io/badge/UI-Streamlit-ff4b4b?logo=streamlit&logoColor=white)](https://streamlit.io)
[![Issues](https://img.shields.io/github/issues/drake69/spendify)](https://github.com/drake69/spendify/issues)
[![Last commit](https://img.shields.io/github/last-commit/drake69/spendify)](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.*