Skip to content

Pipeline

github-actions[bot] edited this page Mar 17, 2026 · 3 revisions

Spendify β€” Pipeline di elaborazione

Documento tecnico di riferimento. Ogni riga di codice che trasforma una transazione passa per questi stadi, in questo ordine.


Mappa di alto livello

FILE (CSV / XLSX)
        β”‚
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  1. CARICAMENTO   β”‚  parse bytes, encoding, delimiter, header
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       schema in DB?
β”‚  2. SCHEMA DECISION    │──────────────────────────────┐
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                              β”‚
         │ no (Flow 2)                                  │ sì (Flow 1)
         β–Ό                                              β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                              β”‚
β”‚  2b. CLASSIFICAZIONE   β”‚  LLM β†’ DocumentSchema        β”‚
β”‚      DOCUMENTO [RF-01] β”‚                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                              β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                    β”‚
                                    β–Ό
                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                     β”‚  3. NORMALIZZAZIONE       β”‚  date, importi, ID SHA-256
                     β”‚     [RF-02]               β”‚  tipo transazione
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β”‚
                                  β–Ό
                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                     β”‚  4. DEDUP CHECK           β”‚  salta tx giΓ  importate
                     β”‚                           β”‚  (ID calcolato al passo 3)
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β”‚  ← nessuna LLM call su tx giΓ  note
                                  β–Ό
                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                     β”‚  5. PULIZIA DESCRIZIONI   β”‚  LLM estrae controparte
                     β”‚     [RF-02 pre-cat.]      β”‚  (pagante o ricevente)
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β”‚
                                  β–Ό
                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                     β”‚  6. RILEVAMENTO           β”‚  accoppiamento importo+data
                     β”‚     GIROCONTI [RF-04]     β”‚  o nome proprietario
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β”‚
                                  β–Ό
                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                     β”‚  7. RICONCILIAZIONE       β”‚  carta di credito ↔ addebito
                     β”‚     CARTE [RF-03]         β”‚  su conto corrente
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β”‚
                                  β–Ό
                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                     β”‚  8. CATEGORIZZAZIONE      β”‚  regole β†’ LLM β†’ fallback
                     β”‚     [RF-05]               β”‚
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β”‚
                                  β–Ό
                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                     β”‚  9. PERSISTENZA DB        β”‚  upsert idempotente
                     β”‚     [RF-06, RF-07]        β”‚
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                  β”‚
                                  β–Ό
                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                     β”‚  10. REVISIONE MANUALE    β”‚  to_review=True β†’ utente
                     β”‚      + REGOLE [RF-08]     β”‚  β†’ riapplica regole
                     β”‚                           β”‚
                     β”‚      Pagine UI:            β”‚
                     β”‚      β€’ Review             β”‚
                     β”‚      β€’ Modifiche massive  β”‚
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Stadio 1 β€” Caricamento file

Modulo: core/normalizer.py β†’ load_raw_dataframe()

detect_encoding(raw_bytes)
  └─ chardet β†’ alias normalizzato (ascii β†’ utf-8)

Per XLSX / XLS:
  detect_best_sheet(workbook)
    └─ esclude fogli con nome summary/totale/riepilogo
    └─ punteggio = n_righe + (n_colonne_numeriche Γ— 10)
  pd.read_excel(sheet)

Per CSV / testo:
  detect_delimiter(content)
    └─ frequenza dei caratteri [, ; | TAB] β†’ vince il piΓΉ frequente
  detect_header_row(lines) β†’ (skip_rows: int, certain: bool)
    └─ prima riga con β‰₯ 2 campi non-numerici e non-vuoti
    └─ certain=True  β†’ header trovato, usato silenziosamente
    └─ certain=False β†’ nessun match, fallback a 0 (ambiguo β†’ UI chiede conferma)
  pd.read_csv(sep=delimiter, skiprows=skip_rows)

Dopo il caricamento (sia CSV che Excel), vengono applicati i pre-processing Phase 0:

detect_and_strip_preheader_rows(df)
  └─ conta celle non-null per riga β†’ calcola mediana β†’ soglia = mediana Γ— 0.5
  └─ righe contigue in cima con densitΓ  < soglia β†’ rimosse (max 20 righe / 10%)
  └─ la prima riga non-sparsa diventa la nuova intestazione colonne

drop_low_variability_columns(df)
  └─ per ogni colonna: nunique(col) / n_righe < 1.5% β†’ colonna metadata
  └─ colonne candidate rimosse (mai scende sotto 2 colonne)

Output: DataFrame ripulito + PreprocessInfo(skipped_rows, dropped_columns)


Schema fingerprinting β€” header SHA256

Modulo: core/normalizer.py β†’ compute_header_sha256(), load_raw_head()

Per evitare di eseguire il classifier LLM ad ogni import dello stesso formato file, Spendify calcola lo SHA256 delle prime min(30, N) righe raw (prima di qualsiasi skip o pre-elaborazione) e lo memorizza insieme allo schema confermato.

compute_header_sha256(raw_bytes, filename, n=30)
  └─ Excel: prime min(30, N) righe del foglio migliore β†’ serializzate con "|" tra celle
  └─ CSV:   prime min(30, N) righe di testo grezze
  └─ SHA256(contenuto.encode()) β†’ hex string 64 caratteri

load_raw_head(raw_bytes, filename, n=10)
  └─ carica N righe senza skiprows, senza preprocessing
  └─ usato dall'UI di revisione schema per mostrare la struttura raw del file

Algoritmo al re-import:

  1. Calcola header_sha256 delle prime min(30, N) righe raw
  2. Query DB: SELECT * FROM document_schema WHERE header_sha256 = ?
  3. Se trovato β†’ usa lo schema salvato (include skip_rows) β†’ salta classifier e review UI
  4. Se non trovato β†’ Flow 2 (classificazione LLM + review UI obbligatoria)

PerchΓ© le prime righe? I file di estratto conto contengono tipicamente righe di intestazione istituzionale statiche (nome banca, numero conto, intervallo date) identiche in tutti gli export mensili dello stesso istituto. Queste righe sono un fingerprint affidabile del formato.

Rilevamento righe da saltare β€” flusso completo

detect_skip_rows(raw_bytes, filename) β†’ (N, certain)
  β”œβ”€ CSV:   detect_header_row(lines)        β†’ (N, certain)
  └─ Excel: detect_header_row_excel(bytes)  β†’ (N, certain)
              └─ stessa euristica CSV applicata ai valori delle celle

Al caricamento (prima del pulsante Elabora):
  1. compute_header_sha256 β†’ find_schema_by_header_sha256()
     β”œβ”€ HIT  β†’ skip_rows noto dallo schema; nessun input all'utente
     └─ MISS β†’ detect_skip_rows()
               β”œβ”€ certain=True  β†’ N usato silenziosamente (qualunque valore)
               └─ certain=False β†’ UI mostra number_input "Righe da saltare"
                                  (default=0, utente puΓ² correggere)

skip_rows_override β€” process_file accetta skip_rows_override: int | None (dal form UI). Precede sempre known_schema.skip_rows. load_raw_dataframe accetta lo stesso parametro:

  • CSV: sostituisce detect_header_row()
  • Excel: passa skiprows=N a pd.read_excel e salta detect_and_strip_preheader_rows()

Stadio 2 β€” Schema decision / Classificazione documento [RF-01]

Modulo: core/orchestrator.py, core/classifier.py

Flow 1 β€” schema giΓ  in DB

_schema_is_usable(known_schema)
  └─ richiede: date_col AND (amount_col OR (debit_col AND credit_col))
  └─ se valido β†’ salta classificazione

Flow 2 β€” sorgente nuova, LLM richiesto

classify_document(df_raw, llm_backend)

  FASE 0 β€” Python, deterministico
    └─ Sinonimi colonne (nessuna LLM):
       data_col  β†’ data, date, data operazione, buchungsdatum, …
       amount_col β†’ importo, amount, betrag, montant, …
       debit/credit β†’ dare/avere, addebiti/accrediti, uscite/entrate, …
       description β†’ descrizione, causale, memo, payee, …

  FASE 0.5 β€” Ispezione segni
    └─ Se amount_col semantica "neutral":
       legge dati reali β†’ se qualsiasi valore < 0 β†’ invert_sign=False certo

  FASE 1 β€” LLM, campi ambigui
    input:
      - nomi colonne
      - prime 20 righe (dati sensibili redatti)
      - risultati Fase 0 (come fatti certi)
    output JSON:
      {
        doc_type:   bank_account | credit_card | debit_card | prepaid_card | savings | unknown
        date_format: strptime pattern (es. %d/%m/%Y)
        sign_convention: signed_single | debit_positive | credit_negative
        invert_sign: true/false  (carte: spese tipicamente positive nel CSV)
        internal_transfer_patterns: ["bonifico", "giroconto", …]
      }

  POST-LLM β€” Fase 0 prevale su LLM
    └─ merge: i risultati certi della Fase 0 sovrascrivono il LLM
    └─ safety: se doc_type = carta β†’ invert_sign=True forzato

Output: DocumentSchema con mappatura colonne e convenzioni segno

Schema review gate (Flow 2 obbligatorio)

Al primo import di un file sconosciuto (header SHA256 non trovato in DB), l'importazione si ferma sempre β€” indipendentemente dalla confidenza del classifier β€” e mostra all'utente un form di revisione con:

  • Preview raw: prime 10 righe del file senza preprocessing (via load_raw_head())
  • Campi dello schema: doc_type, account_label, colonna importo, data, segno, addebiti/accrediti, inverti segno
  • Preview parsata: prime 8 transazioni elaborate con lo schema corrente β€” si aggiorna live ad ogni modifica
  • Pulsante "Conferma e importa": salva lo schema (con header_sha256) e avvia l'import

Dal secondo import dello stesso formato, il header_sha256 viene trovato in DB e l'intero processo Γ¨ automatico (nessuna LLM call, nessuna UI).


Stadio 3 β€” Normalizzazione [RF-02]

Modulo: core/normalizer.py β†’ _normalize_df_with_schema()

Per ogni riga del DataFrame:

parse_date_safe(valore, formato)
  └─ prova formato schema β†’ fallback a formati comuni IT/ISO/US
  └─ None se fallisce (riga scartata)

apply_sign_convention(riga, convention)
  β”œβ”€ signed_single:    usa amount_col cosΓ¬ com'Γ¨
  β”œβ”€ debit_positive:   credito βˆ’ debito  (entrambi positivi nel CSV)
  └─ credit_negative:  credito as-is, βˆ’|debito|

parse_amount(valore)
  β”œβ”€ "1.234,56" (EU)  β†’ 1234.56
  β”œβ”€ "1,234.56" (US)  β†’ 1234.56
  └─ "1234,56"        β†’ 1234.56

normalize_description(testo)
  └─ NFC unicode + casefold + strip

compute_transaction_id(account_label, data_raw, importo_raw, descrizione_raw)
  └─ SHA-256[:24] su valori GREZZI
  └─ stabile tra versioni di normalizzazione

_infer_tx_type(importo, doc_type, descrizione, pattern_interni)
  β”œβ”€ matcha pattern_interni β†’ internal_out (< 0) / internal_in (β‰₯ 0)
  β”œβ”€ carta di credito/debito/prepagata β†’ card_tx
  └─ altrimenti: income (β‰₯ 0) / expense (< 0)

Dedup intra-file:

Righe con stessa (account_label + data + importo + descrizione)
  β†’ somma importi, ricalcola hash
  (evita doppio conteggio se la stessa tx appare piΓΉ volte nell'export)

Rimozione riga saldo carta:

remove_card_balance_row(txs, epsilon)
  └─ rileva la riga il cui |importo| β‰ˆ Ξ£|altri importi|
  └─ con owner_label β†’ rinomina descrizione (il rilevamento giroconti la cattura)
  └─ senza owner_label β†’ rimuove la riga

Output: lista dict transazioni con tutti i campi canonici, raw_description immutabile


Stadio 4 β€” Dedup check

Modulo: db/repository.py β†’ get_existing_tx_ids()

Gli ID transazione sono calcolati al passo 3 da valori grezzi, quindi la dedup avviene prima di qualsiasi chiamata LLM: nessun token sprecato su tx giΓ  importate.

existing_ids = query DB WHERE id IN (tutti_gli_id_del_batch)
β†’ filtra le tx giΓ  presenti
β†’ se tutte presenti β†’ abort early (file giΓ  importato, zero LLM calls)
β†’ prosegue solo con le tx nuove

Stadio 5 β€” Pulizia descrizioni [RF-02, pre-categorizzazione]

Modulo: core/description_cleaner.py β†’ clean_descriptions_batch()

Estrae il nome della controparte dalla stringa grezza della banca.

Divisione per segno:
  spese (importo < 0) β†’ PASS 1: estrai DESTINATARIO
  entrate (importo β‰₯ 0) β†’ PASS 2: estrai MITTENTE

Privacy (obbligatoria prima di ogni LLM call):
  redact_pii(descrizione, sanitize_config)
    β”œβ”€ Nomi proprietari β†’ nomi fittizi plausibili (pool per lingua)
    β”‚    IT: Carlo Brambilla, Marta Pellegrino, …
    β”‚    EN: James Fletcher, Helen Norris, …
    β”‚    DE: Klaus Hartmann, Monika Braun, …
    β”‚    FR: Pierre Dumont, Claire Lebrun, …
    β”œβ”€ IBAN β†’ <ACCOUNT_ID>
    β”œβ”€ PAN / carta (13-19 cifre) β†’ <CARD_ID>
    β”œβ”€ Carta mascherata (****0178) β†’ <CARD_ID>
    β”œβ”€ Codici transazione (CAU, NDS, CRO, RIF, TRN…) β†’ <TX_CODE>
    └─ Codice fiscale β†’ <FISCAL_ID>

  LLM elabora descrizione redatta

  restore_owner_placeholders(risultato_llm)
    └─ riporta i nomi fittizi β†’ nomi reali del proprietario

Cosa il LLM deve eliminare:

- Etichette tipo pagamento: POS, Bonifico, Virement, Lastschrift, SCT, wire transfer
- Marcatori beneficiario: Fv., F.V., Beg., BegΓΌnstigter, Pour, For the benefit of
- VOSTRA DISPOSIZIONE, Disposizione
- Importi e valute: "352,00 EUR", "9.798,76 EUR"
- Date: "23.12.2025", "2025-12-29", "29/10.41"
- Numeri carta, codici auth (CAU/NDS), riferimenti (RIF:/CRO:/INV/)
- Token ORD., codici paese (ITA)(FRA)
- CittΓ  dopo il nome dell'azienda
- Frasi duplicate: "Rimborso spese rimborso spese" β†’ "Rimborso spese"

Spese origine bancaria (nessuna controparte esterna):

β†’ etichetta nella lingua configurata:
   IT: "Interessi bancari", "Commissioni bancarie"
   EN: "Bank fees", "Bank interest"
   FR: "Frais bancaires", "IntΓ©rΓͺts bancaires"
   DE: "BankgebΓΌhren", "Bankzinsen"

Fallback: se LLM fallisce β†’ mantieni raw_description originale

Output: transaction["description"] aggiornato; raw_description mai modificato


Stadio 6 β€” Rilevamento giroconti [RF-04]

Modulo: core/normalizer.py β†’ detect_internal_transfers()

FASE 1 β€” Accoppiamento tra conti diversi
  Per ogni coppia (i, j) con i.account_label β‰  j.account_label:

    amount_match = |importo_i + importo_j| ≀ epsilon          (0.01 €)
    date_match   = |data_i βˆ’ data_j| ≀ delta_days             (5 gg)

    Se entrambi:
      high_symmetry = |importo_i + importo_j| ≀ epsilon_strict (0.005 €)
                    AND |data_i βˆ’ data_j| ≀ delta_days_strict  (1 gg)

      Confidenza:
        HIGH   β†’ keyword "bonifico/giroconto/transfer/…" in descrizione
        MEDIUM β†’ high_symmetry senza keyword

      Se require_keyword_confirmation=True AND confidenza=MEDIUM:
        β†’ segna transfer_pair_id ma NON aggiorna tx_type (to_review)
      Altrimenti:
        β†’ aggiorna tx_type: internal_out (uscita) / internal_in (entrata)

FASE 2 β€” Match per nome proprietario (tx non ancora accoppiate)
  Per ogni tx senza coppia:
    Se la descrizione contiene un nome proprietario
    (regex con tutte le permutazioni dei token del nome):
      β†’ tx_type = internal_out / internal_in
      β†’ transfer_confidence = HIGH
      (il proprietario Γ¨ la controparte: nessun accoppiamento necessario)

Parametri chiave:

Parametro Default Significato
tolerance 0.01 € epsilon importo
tolerance_strict 0.005 € epsilon strict
settlement_days 5 gg finestra date
settlement_days_strict 1 gg finestra strict

Stadio 7 β€” Riconciliazione carte [RF-03]

Modulo: core/normalizer.py β†’ find_card_settlement_matches()

Abbina gli addebiti card_settlement (dal conto corrente) alle singole card_tx (dalla carta).

Per ogni addebito:

  FASE 1 β€” Finestra temporale
    └─ card_tx in [data_addebito βˆ’ 45 gg, data_addebito + 7 gg]

  FASE 2 β€” Sliding window (sottoinsiemi contigui)
    Per ogni sottoinsieme contiguo [i..j]:
      β”œβ”€ verifica gap tra tx consecutive ≀ max_gap_days (5 gg)
      β”œβ”€ somma = Ξ£ |importo[i..j]|
      └─ Se |somma βˆ’ importo_addebito| ≀ epsilon β†’ MATCH βœ“

  FASE 3 β€” Subset sum ai bordi (fallback)
    β”œβ”€ prende le k=10 tx prima + k=10 tx dopo la data addebito
    β”œβ”€ ricerca esaustiva su tutti i sottoinsiemi (n ≀ 20 β†’ 2^20 β‰ˆ 1M, sicuro)
    └─ Se qualsiasi sottoinsieme somma all'importo β†’ MATCH βœ“

  Se MATCH trovato:
    β†’ ReconciliationLink {settlement_id, matched_ids, delta, method}
    β†’ tx matched: reconciled=True

Stadio 8 β€” Categorizzazione [RF-05]

Modulo: core/categorizer.py β†’ categorize_batch()

Elabora solo expense, income, card_tx, unknown. Salta giroconti e card_settlement.

Per ogni transazione β€” cascata a 4 livelli:

  LIVELLO 0 β€” Regole utente (CategoryRule, ordinate per prioritΓ )
  ──────────────────────────────────────────────────────────────
  Per ogni regola (in ordine di prioritΓ  decrescente):
    CategoryRule.matches(descrizione, doc_type):
      β”œβ”€ exact:    descrizione.casefold() == pattern.casefold()
      β”œβ”€ contains: pattern.casefold() IN descrizione.casefold()
      └─ regex:    re.search(pattern, descrizione.casefold())

    Se doc_type specificato nella regola β†’ deve coincidere

    VINCE la prima regola che fa match β†’
      categoria, sottocategoria, confidenza=HIGH, sorgente=rule, to_review=False

  LIVELLO 1 β€” Regole statiche per parola chiave (direction-aware)
  ───────────────────────────────────────────────────────────────
  Pattern hardcoded, separati per spese/entrate:

  SPESE:
    conad|coop|esselunga|lidl|carrefour|…  β†’ Alimentari / Spesa supermercato
    farmacia|pharma|…                      β†’ Salute / Farmaci
    eni|shell|q8|tamoil|…                  β†’ Trasporti / Carburante
    telepass|autostrad|…                   β†’ Trasporti / Parcheggio e ZTL
    trenitalia|italo|frecciarossa|…        β†’ Trasporti / Trasporto pubblico
    enel|iren|a2a|hera|…                   β†’ Casa / Energia elettrica
    netflix|spotify|amazon prime|…         β†’ Svago / Streaming

  ENTRATE:
    stipendio|salary|busta paga|…          β†’ Lavoro dipendente / Stipendio
    pensione|inps rendita|…                β†’ Prestazioni sociali / Pensione

    β†’ confidenza=HIGH, sorgente=rule, to_review=False

  LIVELLO 2 β€” Modello ML (stub)
  ──────────────────────────────
  β†’ restituisce None (riservato a sviluppi futuri)

  LIVELLO 3 β€” LLM (due batch direzionali)
  ────────────────────────────────────────
  Batch separati per spese ed entrate.

  Privacy:
    redact_pii(descrizione) prima di inviare al LLM

  Payload per ogni tx:
    {"amount": "βˆ’352.00", "description": "Notorious Cinemas"}

  Risposta attesa:
    {
      "results": [
        {
          "category": "Svago e tempo libero",
          "subcategory": "Cinema e teatro",
          "confidence": "high",
          "rationale": "Cinema"
        },
        …
      ]
    }

  Validazione risposta LLM:
    β”œβ”€ categoria + sottocategoria valida nella tassonomia?
    β”œβ”€ direzione corretta (spesa per spese, entrata per entrate)?
    β”œβ”€ Se sottocategoria non trovata β†’ cerca categoria padre
    β”œβ”€ Se categoria non trovata β†’ primo sub valido per quella categoria
    └─ Se correzione necessaria β†’ confidenza=low, to_review=True

  Livelli confidenza:
    HIGH   β†’ to_review=False
    MEDIUM β†’ to_review=False (sopra threshold 0.80)
    LOW    β†’ to_review=True

  LIVELLO 4 β€” Fallback (tutto fallisce)
  ──────────────────────────────────────
  spese:   categoria=Altro,         sub=Spese non classificate
  entrate: categoria=Altro entrate, sub=Entrate non classificate
  confidenza=LOW, sorgente=llm, to_review=True

Stadio 9 β€” Persistenza DB [RF-06, RF-07]

Modulo: db/repository.py β†’ persist_import_result()

Tutto in una transazione atomica, ogni operazione Γ¨ idempotente:

create_import_batch(sha256, filename, flow_used, n_transactions)
  └─ se sha256 esiste giΓ  β†’ return existing (file giΓ  importato)

upsert_document_schema(schema)
  └─ se source_identifier esiste β†’ aggiorna; altrimenti crea

Per ogni transazione:
  upsert_transaction(tx)
    └─ se tx.id esiste β†’ skip (dedup finale)
    └─ altrimenti: INSERT con tutti i campi

Per ogni riconciliazione:
  create_reconciliation_link(settlement_id, detail_id, delta, method)
  update tx: reconciled=True

Per ogni giroconto:
  create_transfer_link(out_id, in_id, confidence, keyword_matched)

session.commit()

Stadio 10 β€” Revisione manuale e regole [RF-08]

Pagina: ui/review_page.py, ui/rules_page.py

Auto-applicazione regole (ad ogni caricamento pagina Review):
  apply_rules_to_review_transactions(session, user_rules)
    └─ per ogni tx con to_review=True:
       └─ prima regola che fa match β†’
          categoria, sorgente=rule, to_review=False

Pulsante "▢️ Esegui tutte le regole" (pagina Regole):
  apply_all_rules_to_all_transactions(session, user_rules)
    └─ applica tutte le regole a TUTTE le transazioni (non solo to_review=True)
    └─ regole in ordine di prioritΓ  decrescente, primo match vince
    └─ restituisce (n_matched, n_cleared_review)
    └─ richiede conferma tramite checkbox prima dell'esecuzione

Pulsante "Rielabora con LLM" (pagina Review):
  _rerun_llm_on_review(engine)
    └─ carica tutte le tx con to_review=True
       (esclusi giroconti e card_settlement)
    └─ ri-esegue clean_descriptions_batch()
    └─ ri-esegue categorize_batch()
       (salta le tx con category_source=manual o rule)

Correzione manuale:
  update_transaction_category(tx_id, categoria, sotto)
    └─ category_source=manual, to_review=False

Creazione regola:
  create_category_rule(pattern, match_type, categoria, sotto, prioritΓ )
    └─ si propaga immediatamente a tutte le tx simili

Bulk edit descrizione:
  _apply_description_rule_bulk(engine, pattern, match_type, nuova_desc)
    └─ aggiorna description per tutte le tx con raw_description matching
    └─ ri-categorizza con LLM

Tabella riepilogativa sorgenti di categoria

Sorgente (category_source) Significato to_review
rule Regola utente o keyword statica False
llm confidenza HIGH/MEDIUM LLM sopra threshold False
llm confidenza LOW LLM sotto threshold True
manual Correzione manuale utente False
llm fallback (Altro) Tutto fallito True

Parametri globali di configurazione

Parametro Default Dove si imposta
llm_backend local_ollama Impostazioni
description_language it Impostazioni
confidence_threshold 0.80 ProcessingConfig
tolerance (importo trasferimento) 0.01 € ProcessingConfig
settlement_days 5 gg ProcessingConfig
window_days (riconciliazione carte) 45 gg ProcessingConfig
require_keyword_confirmation True ProcessingConfig
owner_names β€” Impostazioni
batch_size (LLM) 20 tx/chiamata categorize_batch()

Clone this wiki locally