{"id":48997673,"url":"https://github.com/drake69/spendify","last_synced_at":"2026-04-18T17:14:44.653Z","repository":{"id":344894964,"uuid":"1139746777","full_name":"drake69/spendify","owner":"drake69","description":"🏦 Personal finance ledger — aggregates bank statements (CSV/XLSX) into a single ledger with hybrid deterministic + LLM categorization. Offline-first, privacy-safe.","archived":false,"fork":false,"pushed_at":"2026-04-08T13:15:39.000Z","size":2833,"stargazers_count":2,"open_issues_count":28,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-08T15:06:37.164Z","etag":null,"topics":["bank-statements","budgeting","finance","llm","ollama","personal-finance","python","self-hosted","sqlite","streamlit"],"latest_commit_sha":null,"homepage":"https://drake69.github.io/spendify","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/drake69.png","metadata":{"files":{"readme":"README.it.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":"support/core_logic.py","governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-22T11:08:02.000Z","updated_at":"2026-03-27T21:40:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/drake69/spendify","commit_stats":null,"previous_names":["drake69/spendify"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/drake69/spendify","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/drake69%2Fspendify","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/drake69%2Fspendify/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/drake69%2Fspendify/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/drake69%2Fspendify/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/drake69","download_url":"https://codeload.github.com/drake69/spendify/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/drake69%2Fspendify/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31976905,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T16:27:12.723Z","status":"ssl_error","status_checked_at":"2026-04-18T16:27:11.140Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bank-statements","budgeting","finance","llm","ollama","personal-finance","python","self-hosted","sqlite","streamlit"],"created_at":"2026-04-18T17:14:44.528Z","updated_at":"2026-04-18T17:14:44.647Z","avatar_url":"https://github.com/drake69.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Spendify v3.0\n\n[![CI](https://github.com/drake69/spendify/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/drake69/spendify/actions/workflows/ci.yml)\n[![codecov](https://codecov.io/gh/drake69/spendify/graph/badge.svg)](https://codecov.io/gh/drake69/spendify)\n[![Python 3.13](https://img.shields.io/badge/python-3.13-blue?logo=python\u0026logoColor=white)](https://www.python.org/downloads/)\n[![License: PolyForm NC](https://img.shields.io/badge/license-PolyForm%20Noncommercial-orange)](LICENSE)\n[![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)\n[![Streamlit](https://img.shields.io/badge/UI-Streamlit-ff4b4b?logo=streamlit\u0026logoColor=white)](https://streamlit.io)\n[![Issues](https://img.shields.io/github/issues/drake69/spendify)](https://github.com/drake69/spendify/issues)\n[![Last commit](https://img.shields.io/github/last-commit/drake69/spendify)](https://github.com/drake69/spendify/commits/main)\n\n\u003e 🇬🇧 [Read in English](README.md)\n\nRegistro finanziario personale unificato con pipeline ibrida deterministica + LLM.\n\nAggrega 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.\n\n---\n\n## Indice\n\n- [Caratteristiche principali](#caratteristiche-principali)\n- [Architettura](#architettura)\n- [Struttura del progetto](#struttura-del-progetto)\n- [Installazione](#installazione)\n- [Configurazione](#configurazione)\n- [Avvio](#avvio)\n- [Tassonomia](#tassonomia)\n- [Motore delle regole](#motore-delle-regole)\n- [Giroconti](#giroconti)\n- [Test](#test)\n- [Decisioni di design](#decisioni-di-design)\n\n---\n\n## Caratteristiche principali\n\n| Funzionalità | Dettaglio |\n|---|---|\n| **Classificazione automatica** | Rileva tipo di documento (conto corrente, carta, prepagata, deposito) senza configurazione preventiva |\n| **Normalizzazione deterministica** | Encoding detection, delimiter detection, header detection, importi in `Decimal` (mai `float`) |\n| **Correzione segno carta** | Flag `invert_sign` in `DocumentSchema`: quando un file carta salva le spese come valori positivi, vengono negati automaticamente |\n| **Idempotenza SHA-256** | Re-importare lo stesso file produce esattamente lo stesso insieme di righe |\n| **Riconciliazione carta–c/c (RF-03)** | Algoritmo a 3 fasi che elimina il double-counting da addebiti aggregati mensili |\n| **Rilevamento giroconti (RF-04)** | Matching simbolico importo+finestra temporale; esclusione o neutralizzazione configurabile |\n| **Categorizzazione a cascata (RF-05)** | Regole utente → regex statiche → LLM strutturato → fallback \"Altro\" |\n| **Motore regole con applicazione retroattiva** | Le regole deterministiche vengono applicate a tutte le transazioni esistenti al momento del salvataggio, non solo alle future importazioni |\n| **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 |\n| **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). |\n| **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. |\n| **Tassonomia a 2 livelli nel DB** | 15 categorie di spesa + 7 di entrata; gestita dalla pagina Tassonomia (DB-backed, nessun restart richiesto) |\n| **Backend LLM multi-provider** | Ollama (locale, default), OpenAI, Claude — interfaccia astratta comune, nessun LangChain |\n| **Config LLM nell'UI** | Backend, modello e chiavi API configurabili dalla pagina Impostazioni senza toccare `.env` |\n| **PII sanitization (RF-10)** | IBAN, PAN, CF, nomi del titolare redatti prima di qualsiasi chiamata remota |\n| **Circuit breaker** | Fallback automatico su Ollama locale; quarantena (`to_review=True`) se tutti i backend falliscono |\n| **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 |\n| **Re-run LLM su fallimenti** | Pulsante nella pagina Review che rielabora solo le transazioni in cui l'LLM aveva fallito (`description == raw_description`) |\n| **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 |\n| **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 |\n| **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. |\n| **Persistenza SQLAlchemy** | 11 tabelle ORM; CRUD idempotente; migrazioni automatiche all'avvio |\n| **Progresso import cross-session** | Stato del job di importazione salvato nel DB; tutte le sessioni browser vedono il progresso in tempo reale |\n| **Export report** | HTML standalone (Plotly), CSV, XLSX |\n| **UI Streamlit 9 pagine** | Import → Ledger → Modifiche massive → Analytics → Review → Regole → Tassonomia → Impostazioni → Check List |\n| **Check List mensile** | Tabella pivot mese × conto con conteggio transazioni; evidenzia i mesi mancanti a colpo d'occhio |\n\n---\n\n## Architettura\n\n```\n┌──────────────────────────────────────────────────────────────────────────┐\n│                            app.py  (Streamlit)                           │\n│  [onboarding gate] → sidebar → upload │ ledger │ bulk-edit │ analytics  │\n│                               review │ rules │ taxonomy │ settings       │\n└──────────────────────────┬───────────────────────────────────────────────┘\n                           │ services.*  (facade layer)\n               core/orchestrator.py\n               ProcessingConfig  ·  process_file()\n                           │\n        ┌──────────────────┼───────────────────┐\n        │                  │                   │\n Flow 1 (template)    Flow 2 (schema-on-read)\n DocumentSchema        classifier.py → LLM  → DocumentSchema\n già noto              (campione sanitizzato)    invert_sign detection\n        │\n normalizer.py          sanitizer.py      llm_backends.py\n ├─ encoding detect     ├─ IBAN/PAN/CF    ├─ OllamaBackend\n ├─ parse_amount()      ├─ owner names    ├─ OpenAIBackend\n ├─ SHA-256 tx_id       └─ assert_sani.. └─ ClaudeBackend\n ├─ invert_sign                              BackendFactory\n ├─ RF-03 reconcile                          call_with_fallback()\n └─ RF-04 transfers\n        │\n categorizer.py  ←── TaxonomyConfig (caricato dal DB)\n Step 0: regole utente  (risoluzione sottocategoria → categoria)\n Step 1: regex statiche\n Step 2: stub ML\n Step 3: LLM structured output  (enum sottocategorie vincolato)\n Step 4: fallback \"Altro\"\n        │\n    db/repository.py   (SQLAlchemy, idempotente)\n    └─ Transaction · ImportBatch · DocumentSchemaModel\n       ReconciliationLink · InternalTransferLink · CategoryRule\n       UserSettings · ImportJob · Account\n       TaxonomyCategory · TaxonomySubcategory · TaxonomyDefault\n        │\n    reports/generator.py\n    └─ HTML (Jinja2+Plotly) · CSV · XLSX\n```\n\n### Flow 1 vs Flow 2\n\n| | Flow 1 | Flow 2 |\n|---|---|---|\n| **Attivazione** | `DocumentSchema` già in DB per quel fingerprint colonne | Prima importazione di un nuovo formato |\n| **Schema** | Recuperato da DB, applicato direttamente | LLM inferisce lo schema da un campione anonimizzato |\n| **Promozione** | — | Il template Flow 2 approvato viene salvato e diventa Flow 1 |\n| **Costo LLM** | Zero (solo categorizzazione) | Una chiamata per classificazione + una per categorizzazione batch |\n\n---\n\n## Struttura del progetto\n\n```\nspendify/\n├── app.py                  # Entry point Streamlit — onboarding gate + 9 pagine\n├── .env.example            # Template variabili d'ambiente\n├── pyproject.toml          # Dipendenze (uv / pip)\n│\n├── core/\n│   ├── models.py           # Enum: DocumentType, TransactionType, GirocontoMode …\n│   ├── schemas.py          # DocumentSchema (Pydantic) + invert_sign + llm_json_schema()\n│   ├── llm_backends.py     # LLMBackend ABC · Ollama · OpenAI · Claude · BackendFactory\n│   ├── sanitizer.py        # PII redaction (RF-10)\n│   ├── normalizer.py       # Encoding, parse_amount (Decimal), SHA-256, RF-03, RF-04\n│   ├── classifier.py       # Flow 2: inferenza DocumentSchema via LLM\n│   ├── categorizer.py      # Cascata 4-step + TaxonomyConfig (find_category_for_subcategory)\n│   └── orchestrator.py     # Pipeline principale: ProcessingConfig · process_file()\n│\n├── db/\n│   ├── models.py           # ORM SQLAlchemy (11 tabelle) + migrazioni automatiche\n│   ├── repository.py       # CRUD idempotente · persist_import_result() · CRUD tassonomia\n│   │                       #   bulk_set_giroconto_by_description()\n│   │                       #   get_transactions_by_rule_pattern()\n│   │                       #   seed_user_taxonomy_from_default()\n│   └── taxonomy_defaults.py  # Template tassonomia built-in (5 lingue: it/en/fr/de/es)\n│                              #   TAXONOMY_DEFAULTS · SUPPORTED_LANGUAGES\n│\n├── services/               # Facade layer — la UI importa solo da qui, mai da core.* o db.*\n│   ├── settings_service.py # SettingsService: CRUD impostazioni + tassonomia + conti + onboarding\n│   └── import_service.py   # ImportService: analisi file, pipeline, cache schema + re-export tipi\n│\n├── reports/\n│   ├── generator.py        # HTML (Jinja2+Plotly) · CSV · XLSX\n│   └── template_report.html.j2\n│\n├── ui/\n│   ├── onboarding_page.py  # Wizard primo avvio (4 step: lingua → titolari → conti → conferma)\n│   ├── sidebar.py          # Pulsanti navigazione (9 pagine) + modalità giroconto\n│   ├── upload_page.py      # Import multi-file + progress bar cross-session\n│   ├── registry_page.py    # Ledger filtrabile + selezione al click + bulk giroconto\n│   ├── analysis_page.py    # 7 grafici Plotly: barre mensili, saldo cumulativo,\n│   │                       #   pie+treemap spese, drill-down categoria, pie+treemap entrate,\n│   │                       #   top-10 descrizioni, stacked per conto + export HTML\n│   ├── review_page.py      # Correzione categoria + toggle giroconto + salvataggio regola\n│   ├── bulk_edit_page.py   # Operazioni massive: categoria/contesto/giroconto + eliminazione da filtro\n│   ├── rules_page.py       # CRUD completo regole + \"Esegui tutte le regole\" bulk re-categorizzazione\n│   ├── taxonomy_page.py    # CRUD DB-backed per categorie e sottocategorie\n│   ├── settings_page.py    # Locale, lingua, config LLM, expander reset tassonomia\n│   └── checklist_page.py   # Pivot mese × conto: checklist presenza transazioni\n│\n├── tools/\n│   ├── coupling_check.py   # Validatore accoppiamento architetturale (UI → services.* only)\n│   │                       #   --strict: applica baseline, usato in CI\n│   └── coupling_baseline.json  # Soglie max violazioni per file (attualmente tutte zero)\n│\n├── prompts/\n│   ├── classifier.json     # Prompt Flow 2 (hint invert_sign per file carta)\n│   └── categorizer.json    # Prompt categorizzazione transazioni\n│\n├── tests/\n│   ├── test_normalizer.py          # Test deterministici (parse_amount, SHA-256 …)\n│   ├── test_backends.py            # Factory backend, validazione, mock Ollama\n│   ├── test_categorizer.py         # Regole statiche, cascata, risoluzione tassonomia\n│   ├── test_repository_rules.py    # Upsert regole, pattern matching, toggle giroconto, bulk ops\n│   └── test_taxonomy_onboarding.py # Template multi-lingua + servizi onboarding (27 test)\n│\n└── support/\n    ├── formatting.py       # format_amount_display, format_date_display, format_raw_amount_display\n    └── logging.py\n```\n\n---\n\n## Installazione\n\n### ⚡ Installazione rapida (Docker — niente git clone)\n\nL'unico prerequisito è **[Docker Desktop](https://www.docker.com/products/docker-desktop/)** installato e avviato.\n\n**Mac / Linux:**\n```bash\ncurl -fsSL https://raw.githubusercontent.com/drake69/spendify/main/installer/install.sh | bash\n```\n\n**Windows (PowerShell):**\n```powershell\nirm https://raw.githubusercontent.com/drake69/spendify/main/installer/install.ps1 | iex\n```\n\nLo script scarica l'immagine pre-compilata da GitHub Container Registry, avvia il container e apre il browser su **http://localhost:8501** automaticamente.\n\n\u003e **AI locale opzionale:** l'installer chiede se aggiungere Ollama + `gemma3:12b` (scaricato automaticamente, ~8 GB). Compatibile con Apple Silicon (arm64) e amd64.\n\n\u003e **Aggiornamento all'ultima versione:**\n\u003e ```bash\n\u003e docker compose --project-directory ~/spendify pull \u0026\u0026 docker compose --project-directory ~/spendify up -d\n\u003e ```\n\n\u003e **Disinstallazione:** `curl -fsSL https://raw.githubusercontent.com/drake69/spendify/main/installer/uninstall.sh | bash`\n\n---\n\n### Installazione developer (nativa, consigliata su Mac)\n\n\u003e Setup completo, convenzioni di codice, sistema di priorità e flusso PR → **[CONTRIBUTING.md](CONTRIBUTING.md)**\n\n### Prerequisiti\n\n- **Python 3.13+**\n- **[uv](https://github.com/astral-sh/uv)** (gestore pacchetti consigliato)\n- **[Ollama](https://ollama.com)** per il backend LLM locale (default)\n\n### 1. Clona il repository\n\n```bash\ngit clone https://github.com/drake69/spendify.git\ncd spendify\n```\n\n### 2. Installa le dipendenze\n\n```bash\nuv sync\n```\n\n### 3. Configura le variabili d'ambiente\n\n```bash\ncp .env.example .env\n# Nessuna modifica necessaria per un'installazione locale standard — percorsi già impostati\n```\n\n### 4. Scarica il modello LLM locale (opzionale)\n\n```bash\nollama pull gemma3:12b   # ~8 GB — salta se hai intenzione di usare OpenAI/Anthropic\n```\n\n\u003e 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`.\n\n---\n\n## Configurazione\n\nIl 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:\n\n```dotenv\n# URI database — lascia invariato per uso locale; sovrascritto da docker-compose per Docker\nSPENDIFY_DB=sqlite:///ledger.db\n```\n\n\u003e **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.\n\n### Modalità giroconto\n\nConfigurabile dalla sidebar dell'app:\n\n| Modalità | Comportamento |\n|---|---|\n| `neutral` | I giroconti restano nel ledger come `internal_out` / `internal_in` (default) |\n| `exclude` | I giroconti vengono rimossi dal registro (saldo netto non influenzato) |\n\n### Privacy e backend remoti\n\n```\n[LOCAL — default]  Ollama locale: nessun dato esce dal processo.\n                   Nessuna sanitizzazione richiesta.\n\n[REMOTE — opt-in]  OpenAI / Claude: PII sanitization OBBLIGATORIA.\n                   IBAN → \u003cACCOUNT_ID\u003e  |  PAN → \u003cCARD_ID\u003e\n                   CF   → \u003cFISCAL_ID\u003e  |  owner → \u003cOWNER\u003e\n                   Chiamata bloccata se assert_sanitized() fallisce.\n```\n\n---\n\n## Avvio\n\n```bash\n# Con uv\nuv run streamlit run app.py\n\n# Oppure\nstreamlit run app.py\n```\n\nAl **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.\n\nL'app si apre su `http://localhost:8501` con 9 pagine:\n\n| Pagina | Descrizione |\n|---|---|\n| **📥 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). |\n| **📋 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. |\n| **✏️ 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. |\n| **📊 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. |\n| **🔍 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\". |\n| **📏 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. |\n| **🗂️ Tassonomia** | CRUD DB-backed per categorie e sottocategorie (spese e entrate). Le modifiche hanno effetto immediato senza restart. |\n| **⚙️ Impostazioni** | Formato data, separatori importo, lingua descrizioni, contesti di vita, lista conti bancari, backend LLM (modello + chiavi API). Tutto persistito nel DB. |\n| **✅ 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. |\n\n---\n\n## Tassonomia\n\nLa tassonomia è memorizzata nel database (tabelle `taxonomy_category` / `taxonomy_subcategory`) e gestita dalla pagina **🗂️ Tassonomia**.\n\n### Template multi-lingua built-in\n\nLa 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.\n\nPer reimpostare la tassonomia su una lingua diversa in qualsiasi momento: **⚙️ Impostazioni → 🔄 Reset tassonomia**.\n\n### Tassonomia di default (Italiano)\n\n**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\n\n**Categorie di entrata (7):** Lavoro dipendente · Lavoro autonomo · Rendite finanziarie · Rendite immobiliari · Trasferimenti e rimborsi · Prestazioni sociali · Altro entrate\n\n**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.\n\n---\n\n## Motore delle regole\n\nLe regole di categorizzazione sono memorizzate nella tabella `category_rule` e applicate in più punti del ciclo di vita.\n\n### Tipi di matching\n\n| Tipo | Comportamento |\n|---|---|\n| `contains` | Il pattern appare ovunque nella descrizione (case-insensitive) |\n| `exact` | La descrizione corrisponde esattamente al pattern (case-insensitive) |\n| `regex` | Regex Python completa confrontata con la descrizione |\n\n`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.\n\n### Priorità\n\nQuando più regole corrispondono alla stessa transazione vince quella con il valore di `priority` più alto. La priorità di default è 10; è possibile assegnare qualsiasi intero.\n\n### Semantica upsert\n\nCreare una regola con la stessa coppia `(pattern, match_type)` di una regola esistente la **aggiorna** sul posto (categoria, sottocategoria, priorità) anziché creare un duplicato.\n\n### Applicazione retroattiva\n\nSalvare 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.\n\nInoltre, 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.\n\n---\n\n## Giroconti\n\nUn *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.\n\n### Tipi di transazione\n\n| `tx_type` | Significato |\n|---|---|\n| `internal_out` | Lato uscente del giroconto (importo negativo) |\n| `internal_in` | Lato entrante del giroconto (importo positivo) |\n\nEntrambi i tipi sono esclusi dal saldo netto, dalle entrate e dalle uscite.\n\n### Rilevamento automatico (RF-04)\n\nLa pipeline tenta di abbinare i giroconti automaticamente durante l'importazione con tre passaggi:\n\n1. **Regex keyword** — la descrizione corrisponde a un pattern configurato (es. \"Giroconto\", \"Bonifico tra i miei conti\") → alta confidenza\n2. **Matching importo + data** — stesso importo assoluto entro ±3 giorni, su `account_label` diversi → confidenza media/alta\n3. **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\")\n\n### Riesecuzione cross-account\n\nQuando 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.\n\n### Toggle manuale\n\nDalle pagine **Ledger** o **Review** è possibile contrassegnare manualmente qualsiasi transazione come giroconto (o ripristinarla):\n\n- **Toggle singolo** — cambia il `tx_type` della transazione selezionata (`expense` ↔ `internal_out`, `income` ↔ `internal_in`).\n- **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.\n\n`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.\n\n---\n\n## Contesti di vita\n\nI 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*.\n\n### Design\n\n| Aspetto | Dettaglio |\n|---|---|\n| **Storage** | Colonna `context VARCHAR(64)` nullable sulla tabella `Transaction` |\n| **Ortogonalità** | Indipendente da categoria/sottocategoria — qualsiasi combinazione è valida |\n| **Configurabile** | Aggiunta, rinomina e rimozione contesti dalla pagina **⚙️ Impostazioni** (salvati come JSON in `user_settings`) |\n| **Contesti default** | Quotidianità · Lavoro · Vacanza |\n\n### Assegnazione\n\nDalla pagina **📋 Ledger**, seleziona una transazione e apri il pannello espandibile \"🌍 Assegna contesto\":\n\n1. Scegli un contesto dal menu a discesa (o cancella quello esistente)\n2. 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\n3. Clicca **Applica**\n\n### Filtro\n\nLa barra filtri del registro include un selettore contesto: *tutti*, i singoli valori configurati, o *— nessuno —* (transazioni senza contesto assegnato).\n\n---\n\n## Test\n\n```bash\n# Tutti i test (nessun mock LLM richiesto)\nuv run python -m pytest tests/ -v\n\n# Con coverage\nuv run python -m pytest tests/ --cov=core --cov=db --cov-report=term-missing\n```\n\n### File di test\n\n| File | Copertura |\n|---|---|\n| `test_normalizer.py` | `parse_amount`, dedup SHA-256, encoding detection |\n| `test_backends.py` | Factory backend, validazione, mock Ollama |\n| `test_categorizer.py` | Regole statiche, cascata 4-step, risoluzione tassonomia |\n| `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` |\n| `test_taxonomy_onboarding.py` | Struttura `TAXONOMY_DEFAULTS` (5 lingue), migrazione `taxonomy_default` (seeding + idempotenza), metodi onboarding di `SettingsService`, auto-skip per utenti esistenti |\n\nTutti i test usano un database SQLite in-memory — nessun I/O su file, nessun servizio esterno richiesto.\n\n---\n\n## Decisioni di design\n\n### `Decimal` — mai `float`\n\nTutti gli importi sono `decimal.Decimal`. I float IEEE 754 introducono errori di arrotondamento che falsano saldi e riconciliazioni.\n\n### Idempotenza SHA-256\n\nOgni 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.\n\n### Correzione segno carta (`invert_sign`)\n\nGli 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.\n\n#### Algoritmo di rilevamento in due passi\n\nIl 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.\n\n**Step 0 — Sinonimi del nome colonna (priorità massima)**\n\nIl nome della colonna importo viene confrontato con tre gruppi di sinonimi:\n\n| Gruppo | Esempi di nomi | Decisione |\n|---|---|---|\n| **Sinonimi di uscita** | Uscita, Uscite, Addebito, Addebiti, Pagamento, Spesa, Dare, Importo addebitato | `invert_sign = true` (spese salvate come positivi → negarle) |\n| **Sinonimi di entrata** | Entrata, Entrate, Accredito, Accrediti, Avere, Credito, Importo accreditato | `invert_sign = false` (entrate già positive → nessuna modifica) |\n| **Nomi neutri** | Importo, Amount, Valore, Totale | Nessuna decisione — si procede allo Step 1 |\n\nIl 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.\n\n**Step 1 — Analisi della distribuzione dei segni (solo nomi neutri)**\n\nQuando 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`:\n\n- File carta, maggioranza positivi (\u003e 60 %): le spese sono salvate come positivi (convenzione AMEX / tipici export italiani) → `invert_sign = true`\n- File carta, maggioranza negativi (\u003e 60 %): le spese hanno già il segno corretto → `invert_sign = false`\n- 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`)\n- Conto corrente / deposito: sempre `invert_sign = false`, indipendentemente dalla distribuzione\n\n#### Campi diagnostici\n\nOgni `DocumentSchema` prodotto dal Flow 2 include quattro campi diagnostici per audit e debug:\n\n| Campo | Tipo | Contenuto |\n|---|---|---|\n| `positive_ratio` | `float \\| null` | Frazione di valori \u003e 0 nella colonna importo nel campione |\n| `negative_ratio` | `float \\| null` | Frazione di valori \u003c 0 nella colonna importo nel campione |\n| `semantic_evidence` | `list[str]` | 2–4 frasi brevi dell'LLM che spiegano la decisione |\n| `normalization_case_id` | `str \\| null` | C1 = conto corrente signed_single · C2 = carta invertita · C3 = carta già negativa · C4 = colonne Dare/Avere · C5 = ambiguo |\n\nQuesti campi sono persistiti nella tabella DB `document_schema` e visibili nel riepilogo dello schema Flow 2 nell'UI.\n\n### Sottocategoria come chiave primaria\n\nIl 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.\n\n### Tassonomia nel DB\n\nLa 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.\n\n### PII sanitization come precondizione\n\n`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.\n\n### Circuit breaker e quarantena\n\n`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.\n\n### Nessun LangChain\n\nI backend LLM usano direttamente `openai` SDK, `anthropic` SDK e `requests` (per Ollama). Nessuna dipendenza da framework di orchestrazione LLM.\n\n### RF-03: algoritmo a 3 fasi\n\nLa 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).\n\n---\n\n## Dipendenze principali\n\n| Pacchetto | Versione | Scopo |\n|---|---|---|\n| `streamlit` | ≥ 1.35 | UI |\n| `pandas` | ≥ 2.2 | Elaborazione dati |\n| `sqlalchemy` | ≥ 2.0 | ORM / persistenza |\n| `pydantic` | ≥ 2.0 | Validazione schemi |\n| `openai` | ≥ 1.30 | Backend OpenAI |\n| `anthropic` | ≥ 0.28 | Backend Claude |\n| `requests` | ≥ 2.31 | Backend Ollama |\n| `chardet` | ≥ 5.0 | Encoding detection |\n| `plotly` | ≥ 5.20 | Grafici |\n| `jinja2` | ≥ 3.1 | Template report HTML |\n| `pyyaml` | ≥ 6.0 | Parsing seed taxonomy.yaml |\n| `pytest` | ≥ 8.0 | Test |\n\n---\n\n*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.*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdrake69%2Fspendify","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdrake69%2Fspendify","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdrake69%2Fspendify/lists"}