netgescon-day0/docs/91-database/02-tabelle-collegate/movimenti_bancari.md

68 KiB

💳 MOVIMENTI BANCARI - Riconciliazione Automatica

🎯 OVERVIEW

Sistema per gestione completa dei movimenti bancari con riconciliazione automatica delle registrazioni contabili. Supporta import da file CBI, bonifici SEPA, e matching intelligente delle operazioni condominiali.


🗄️ STRUTTURA MYSQL

CREATE TABLE movimenti_bancari (
    -- Identificativi
    id                          BIGINT PRIMARY KEY AUTO_INCREMENT,
    uuid                        VARCHAR(36) NOT NULL UNIQUE,            -- UUID per identificazione esterna
    
    -- Gestione Multi-Tenant
    gestione_id                 BIGINT NOT NULL,                        -- FK gestioni.id
    
    -- Dati Bancari
    banca_codice_abi            VARCHAR(5) NOT NULL,                    -- Codice ABI della banca
    banca_denominazione         VARCHAR(255) NOT NULL,                  -- Nome della banca
    conto_corrente_iban         VARCHAR(34) NOT NULL,                   -- IBAN del conto
    conto_corrente_numero       VARCHAR(20) NULL,                       -- Numero conto (legacy)
    
    -- Operazione Bancaria
    data_operazione             DATE NOT NULL,                          -- Data operazione
    data_valuta                 DATE NOT NULL,                          -- Data valuta
    numero_operazione           VARCHAR(50) NULL,                       -- Numero progressivo banca
    codice_operazione           VARCHAR(10) NULL,                       -- Codice tipo operazione (es: 005=bonifico)
    
    -- Importi e Movimento
    importo                     DECIMAL(12,2) NOT NULL,                 -- Importo movimento (+ entrata, - uscita)
    importo_commissioni         DECIMAL(8,2) DEFAULT 0,                 -- Commissioni bancarie
    divisa                      VARCHAR(3) DEFAULT 'EUR',               -- Valuta
    cambio                      DECIMAL(10,6) DEFAULT 1,                -- Tasso di cambio
    
    -- Descrizioni e Riferimenti
    descrizione_banca           TEXT NOT NULL,                          -- Descrizione fornita dalla banca
    descrizione_normalizzata    VARCHAR(500) NULL,                      -- Descrizione processata per matching
    causale                     TEXT NULL,                              -- Causale operazione
    riferimento_operazione      VARCHAR(100) NULL,                      -- Riferimento univoco operazione
    
    -- Controparte
    controparte_nome            VARCHAR(255) NULL,                      -- Nome beneficiario/ordinante
    controparte_iban            VARCHAR(34) NULL,                       -- IBAN controparte
    controparte_bic             VARCHAR(11) NULL,                       -- BIC controparte
    controparte_normalizzata    VARCHAR(255) NULL,                      -- Nome normalizzato per matching
    
    -- Tipologia Movimento
    tipo_movimento              ENUM('ENTRATA','USCITA','GIROCONTO','COMMISSIONE','INTERESSE') NOT NULL,
    categoria_movimento         VARCHAR(50) NULL,                       -- Categoria specifica (es: QUOTA_CONDOMINIALE)
    
    -- Riconoscimento Automatico Condomini (da UNICREDIT e altre banche)
    causale_bancaria_codice     VARCHAR(10) NULL,                       -- Codice causale banca (048=bonifico entrata, 208=uscita, etc.)
    unita_immobiliare_estratta  VARCHAR(20) NULL,                       -- UI estratta (B-1, C4, A26, D/11, etc.)
    scala_estratta              VARCHAR(5) NULL,                        -- Scala estratta (A, B, C, D)
    interno_estratto            VARCHAR(10) NULL,                       -- Interno estratto (1, 23, etc.)
    avviso_pagamento_numero     VARCHAR(20) NULL,                       -- Numero avviso pagamento estratto
    rata_numero                 INT NULL,                               -- Numero rata estratto (1,2,3,4,5,6)
    rata_tipologia              ENUM('CONDOMINIO','RISCALDAMENTO','SUPERCONDOMINIO','LAVORI_FOGNE','LAVORI_VERDE','ALTRO') NULL,
    
    -- Collegamento Incassi Gescon
    incasso_gescon_id           BIGINT NULL,                            -- FK incassi.id (se riconciliato)
    cod_cassa_gescon            VARCHAR(10) NULL,                       -- Codice cassa Gescon (CCB, CON)
    anno_competenza             YEAR NULL,                              -- Anno competenza estratto (2023, 2024)
    periodo_riferimento         VARCHAR(50) NULL,                       -- Periodo riferimento (2022-2023, 23-24, etc.)
    
    -- Pattern Matching Avanzato
    keywords_estratte           JSON NULL,                              -- Keywords riconosciute per matching
    pattern_riconosciuto        VARCHAR(100) NULL,                      -- Pattern utilizzato per riconoscimento
    confidenza_matching         DECIMAL(3,2) DEFAULT 0,                 -- Livello confidenza matching (0-1)
    
    -- Collegamenti Automatici
    soggetto_id                 BIGINT NULL,                            -- FK soggetti.id (se riconosciuto)
    unita_immobiliare_id        BIGINT NULL,                            -- FK unita_immobiliari.id (se riconosciuta)
    contabilita_id              BIGINT NULL,                            -- FK contabilita.id (se riconciliato)
    fattura_elettronica_id      BIGINT NULL,                            -- FK fatture_elettroniche.id (se collegata)
    
    -- Import e Controllo
    fonte_import                ENUM('CBI','SEPA','MT940','CSV','MANUALE') NOT NULL,
    file_import                 VARCHAR(500) NULL,                      -- Nome file di importazione
    hash_import                 VARCHAR(64) NULL,                       -- Hash per prevenire duplicati
    data_import                 TIMESTAMP NULL,                         -- Quando importato
    
    -- Riconciliazione
    stato_riconciliazione       ENUM('NON_RICONCILIATO','RICONCILIATO','PARZIALE','MANUALE','SOSPESO') DEFAULT 'NON_RICONCILIATO',
    contabilita_id              BIGINT NULL,                            -- FK contabilita.id (se riconciliato)
    matching_score              DECIMAL(5,2) NULL,                      -- Score algoritmo matching (0-100)
    riconciliato_da             BIGINT NULL,                            -- FK users.id
    riconciliato_il             TIMESTAMP NULL,                         -- Timestamp riconciliazione
    note_riconciliazione        TEXT NULL,                              -- Note processo riconciliazione
    
    -- Identificazione Condominiale
    unita_immobiliare_id        BIGINT NULL,                            -- FK unita_immobiliari.id (se identificabile)
    soggetto_id                 BIGINT NULL,                            -- FK soggetti.id (se identificabile)
    rata_numero                 INT NULL,                               -- Numero rata se pagamento rateale
    periodo_riferimento         VARCHAR(20) NULL,                       -- Periodo pagamento (es: "2024-01")
    
    -- Anomalie e Sospesi
    anomalia                    BOOLEAN DEFAULT FALSE,                  -- Flag anomalia
    tipo_anomalia               VARCHAR(100) NULL,                      -- Descrizione anomalia
    richiede_intervento         BOOLEAN DEFAULT FALSE,                  -- Richiede intervento manuale
    sospeso_motivo              TEXT NULL,                              -- Motivo sospensione
    
    -- Pattern Recognition
    pattern_riconosciuto        VARCHAR(100) NULL,                      -- Pattern automatico riconosciuto
    confidence_level            DECIMAL(5,2) DEFAULT 0,                 -- Livello confidenza riconoscimento
    keywords_matched            JSON NULL,                              -- Keywords che hanno fatto match
    
    -- Audit e Sistema
    stato                       ENUM('ATTIVO','CANCELLATO','ARCHIVIATO') DEFAULT 'ATTIVO',
    note                        TEXT NULL,
    created_at                  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at                  TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    created_by                  BIGINT NULL,                            -- FK users.id
    updated_by                  BIGINT NULL,                            -- FK users.id
    
    -- Indici
    INDEX idx_gestione (gestione_id),
    INDEX idx_data_operazione (data_operazione),
    INDEX idx_data_valuta (data_valuta),
    INDEX idx_iban (conto_corrente_iban),
    INDEX idx_importo (importo),
    INDEX idx_tipo_movimento (tipo_movimento),
    INDEX idx_stato_riconciliazione (stato_riconciliazione),
    INDEX idx_contabilita (contabilita_id),
    INDEX idx_unita_immobiliare (unita_immobiliare_id),
    INDEX idx_soggetto (soggetto_id),
    INDEX idx_hash_import (hash_import),
    INDEX idx_anomalia (anomalia),
    INDEX idx_pattern (pattern_riconosciuto),
    UNIQUE KEY unique_hash_import (hash_import),
    
    -- Foreign Keys
    FOREIGN KEY (gestione_id) REFERENCES gestioni(id) ON DELETE CASCADE,
    FOREIGN KEY (contabilita_id) REFERENCES contabilita(id) ON DELETE SET NULL,
    FOREIGN KEY (unita_immobiliare_id) REFERENCES unita_immobiliari(id) ON DELETE SET NULL,
    FOREIGN KEY (soggetto_id) REFERENCES soggetti(id) ON DELETE SET NULL,
    FOREIGN KEY (riconciliato_da) REFERENCES users(id) ON DELETE SET NULL,
    FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
    FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL,
    
    -- Constraints
    CONSTRAINT chk_importo_non_zero CHECK (importo != 0),
    CONSTRAINT chk_commissioni_positive CHECK (importo_commissioni >= 0),
    CONSTRAINT chk_matching_score CHECK (matching_score BETWEEN 0 AND 100),
    CONSTRAINT chk_confidence_level CHECK (confidence_level BETWEEN 0 AND 100),
    CONSTRAINT chk_cambio_positivo CHECK (cambio > 0)
);

-- Tabella per configurazione conti bancari
CREATE TABLE conti_bancari_configurazione (
    id                          BIGINT PRIMARY KEY AUTO_INCREMENT,
    gestione_id                 BIGINT NOT NULL,                        -- FK gestioni.id
    
    -- Dati Conto
    denominazione               VARCHAR(255) NOT NULL,                  -- Nome conto (es: "Conto Ordinario")
    iban                        VARCHAR(34) NOT NULL,                   -- IBAN
    bic                         VARCHAR(11) NULL,                       -- BIC/SWIFT
    banca_denominazione         VARCHAR(255) NOT NULL,                  -- Nome banca
    banca_abi                   VARCHAR(5) NOT NULL,                    -- Codice ABI
    banca_cab                   VARCHAR(5) NULL,                        -- Codice CAB
    
    -- Collegamento Contabile
    piano_conto_id              BIGINT NOT NULL,                        -- FK piano_conti.id
    
    -- Configurazione Import
    formato_import_preferito    ENUM('CBI','SEPA','MT940','CSV') DEFAULT 'CBI',
    separatore_csv              VARCHAR(5) NULL,                        -- Separatore per CSV
    encoding_file               VARCHAR(20) DEFAULT 'UTF-8',            -- Encoding file
    
    -- Pattern Automatici
    pattern_entrate             JSON NULL,                              -- Pattern per riconoscere entrate
    pattern_uscite              JSON NULL,                              -- Pattern per riconoscere uscite  
    pattern_commissioni         JSON NULL,                              -- Pattern commissioni
    
    -- Automazioni
    auto_riconcilia_perfetti    BOOLEAN DEFAULT TRUE,                   -- Auto-riconcilia match 100%
    soglia_auto_riconciliazione DECIMAL(5,2) DEFAULT 95.00,             -- Soglia automatica
    
    -- Stato
    attivo                      BOOLEAN DEFAULT TRUE,
    conto_principale            BOOLEAN DEFAULT FALSE,                  -- Se è il conto principale
    note                        TEXT NULL,
    
    -- Audit
    created_at                  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at                  TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    created_by                  BIGINT NULL,
    updated_by                  BIGINT NULL,
    
    -- Indici e Vincoli
    INDEX idx_gestione (gestione_id),
    INDEX idx_iban (iban),
    INDEX idx_piano_conto (piano_conto_id),
    UNIQUE KEY unique_iban_gestione (iban, gestione_id),
    
    FOREIGN KEY (gestione_id) REFERENCES gestioni(id) ON DELETE CASCADE,
    FOREIGN KEY (piano_conto_id) REFERENCES piano_conti(id) ON DELETE RESTRICT,
    FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
    FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
);

-- Tabella per regole di matching automatico
CREATE TABLE regole_matching_bancario (
    id                          BIGINT PRIMARY KEY AUTO_INCREMENT,
    gestione_id                 BIGINT NOT NULL,                        -- FK gestioni.id
    
    -- Configurazione Regola
    nome_regola                 VARCHAR(255) NOT NULL,                  -- Nome descrittivo
    priorita                    INT DEFAULT 50,                         -- Priorità applicazione (1-100)
    attiva                      BOOLEAN DEFAULT TRUE,
    
    -- Condizioni Matching
    pattern_descrizione         TEXT NULL,                              -- Pattern regex descrizione
    pattern_controparte         TEXT NULL,                              -- Pattern regex controparte
    pattern_causale             TEXT NULL,                              -- Pattern regex causale
    importo_min                 DECIMAL(12,2) NULL,                     -- Importo minimo
    importo_max                 DECIMAL(12,2) NULL,                     -- Importo massimo
    tipo_movimento_filtro       VARCHAR(50) NULL,                       -- Filtro tipo movimento
    
    -- Azioni Automatiche
    piano_conto_target_id       BIGINT NULL,                            -- FK piano_conti.id
    categoria_movimento_auto    VARCHAR(50) NULL,                       -- Categoria automatica
    descrizione_standardizzata  VARCHAR(500) NULL,                      -- Descrizione standard
    auto_approva                BOOLEAN DEFAULT FALSE,                  -- Approva automaticamente
    
    -- Pattern Recognition
    keywords_positive           JSON NULL,                              -- Keywords positive per match
    keywords_negative           JSON NULL,                              -- Keywords negative (esclusione)
    peso_matching               DECIMAL(5,2) DEFAULT 10.00,             -- Peso nel calcolo score
    
    -- Configurazione
    note                        TEXT NULL,
    created_at                  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at                  TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    created_by                  BIGINT NULL,
    updated_by                  BIGINT NULL,
    
    -- Indici
    INDEX idx_gestione (gestione_id),
    INDEX idx_priorita (priorita),
    INDEX idx_attiva (attiva),
    INDEX idx_piano_conto_target (piano_conto_target_id),
    
    FOREIGN KEY (gestione_id) REFERENCES gestioni(id) ON DELETE CASCADE,
    FOREIGN KEY (piano_conto_target_id) REFERENCES piano_conti(id) ON DELETE SET NULL,
    FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
    FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL
);

📋 MIGRATION LARAVEL

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;

return new class extends Migration
{
    public function up()
    {
        // Tabella principale movimenti bancari
        Schema::create('movimenti_bancari', function (Blueprint $table) {
            // Identificativi
            $table->id();
            $table->uuid('uuid')->unique();
            
            // Multi-tenant
            $table->foreignId('gestione_id')->constrained('gestioni')->onDelete('cascade');
            
            // Dati bancari
            $table->string('banca_codice_abi', 5);
            $table->string('banca_denominazione');
            $table->string('conto_corrente_iban', 34);
            $table->string('conto_corrente_numero', 20)->nullable();
            
            // Operazione
            $table->date('data_operazione');
            $table->date('data_valuta');
            $table->string('numero_operazione', 50)->nullable();
            $table->string('codice_operazione', 10)->nullable();
            
            // Importi
            $table->decimal('importo', 12, 2);
            $table->decimal('importo_commissioni', 8, 2)->default(0);
            $table->string('divisa', 3)->default('EUR');
            $table->decimal('cambio', 10, 6)->default(1);
            
            // Descrizioni
            $table->text('descrizione_banca');
            $table->string('descrizione_normalizzata', 500)->nullable();
            $table->text('causale')->nullable();
            $table->string('riferimento_operazione', 100)->nullable();
            
            // Controparte
            $table->string('controparte_nome')->nullable();
            $table->string('controparte_iban', 34)->nullable();
            $table->string('controparte_bic', 11)->nullable();
            $table->string('controparte_normalizzata')->nullable();
            
            // Tipologia
            $table->enum('tipo_movimento', ['ENTRATA','USCITA','GIROCONTO','COMMISSIONE','INTERESSE']);
            $table->string('categoria_movimento', 50)->nullable();
            
            // Import
            $table->enum('fonte_import', ['CBI','SEPA','MT940','CSV','MANUALE']);
            $table->string('file_import', 500)->nullable();
            $table->string('hash_import', 64)->nullable()->unique();
            $table->timestamp('data_import')->nullable();
            
            // Riconciliazione
            $table->enum('stato_riconciliazione', [
                'NON_RICONCILIATO','RICONCILIATO','PARZIALE','MANUALE','SOSPESO'
            ])->default('NON_RICONCILIATO');
            $table->foreignId('contabilita_id')->nullable()->constrained('contabilita')->onDelete('set null');
            $table->decimal('matching_score', 5, 2)->nullable();
            $table->foreignId('riconciliato_da')->nullable()->constrained('users')->onDelete('set null');
            $table->timestamp('riconciliato_il')->nullable();
            $table->text('note_riconciliazione')->nullable();
            
            // Identificazione
            $table->foreignId('unita_immobiliare_id')->nullable()->constrained('unita_immobiliari')->onDelete('set null');
            $table->foreignId('soggetto_id')->nullable()->constrained('soggetti')->onDelete('set null');
            $table->integer('rata_numero')->nullable();
            $table->string('periodo_riferimento', 20)->nullable();
            
            // Anomalie
            $table->boolean('anomalia')->default(false);
            $table->string('tipo_anomalia', 100)->nullable();
            $table->boolean('richiede_intervento')->default(false);
            $table->text('sospeso_motivo')->nullable();
            
            // Pattern Recognition
            $table->string('pattern_riconosciuto', 100)->nullable();
            $table->decimal('confidence_level', 5, 2)->default(0);
            $table->json('keywords_matched')->nullable();
            
            // Stato
            $table->enum('stato', ['ATTIVO','CANCELLATO','ARCHIVIATO'])->default('ATTIVO');
            $table->text('note')->nullable();
            
            // Audit
            $table->timestamps();
            $table->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
            $table->foreignId('updated_by')->nullable()->constrained('users')->onDelete('set null');
            
            // Indici
            $table->index(['gestione_id']);
            $table->index(['data_operazione']);
            $table->index(['data_valuta']);
            $table->index(['conto_corrente_iban']);
            $table->index(['importo']);
            $table->index(['tipo_movimento']);
            $table->index(['stato_riconciliazione']);
            $table->index(['anomalia']);
            $table->index(['pattern_riconosciuto']);
        });
        
        // Configurazione conti bancari
        Schema::create('conti_bancari_configurazione', function (Blueprint $table) {
            $table->id();
            $table->foreignId('gestione_id')->constrained('gestioni')->onDelete('cascade');
            
            // Dati conto
            $table->string('denominazione');
            $table->string('iban', 34);
            $table->string('bic', 11)->nullable();
            $table->string('banca_denominazione');
            $table->string('banca_abi', 5);
            $table->string('banca_cab', 5)->nullable();
            
            // Collegamento
            $table->foreignId('piano_conto_id')->constrained('piano_conti')->onDelete('restrict');
            
            // Import
            $table->enum('formato_import_preferito', ['CBI','SEPA','MT940','CSV'])->default('CBI');
            $table->string('separatore_csv', 5)->nullable();
            $table->string('encoding_file', 20)->default('UTF-8');
            
            // Pattern
            $table->json('pattern_entrate')->nullable();
            $table->json('pattern_uscite')->nullable();
            $table->json('pattern_commissioni')->nullable();
            
            // Automazioni
            $table->boolean('auto_riconcilia_perfetti')->default(true);
            $table->decimal('soglia_auto_riconciliazione', 5, 2)->default(95.00);
            
            // Stato
            $table->boolean('attivo')->default(true);
            $table->boolean('conto_principale')->default(false);
            $table->text('note')->nullable();
            
            // Audit
            $table->timestamps();
            $table->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
            $table->foreignId('updated_by')->nullable()->constrained('users')->onDelete('set null');
            
            // Vincoli
            $table->unique(['iban', 'gestione_id'], 'unique_iban_gestione');
            $table->index(['gestione_id']);
            $table->index(['iban']);
        });
        
        // Regole matching automatico
        Schema::create('regole_matching_bancario', function (Blueprint $table) {
            $table->id();
            $table->foreignId('gestione_id')->constrained('gestioni')->onDelete('cascade');
            
            // Configurazione
            $table->string('nome_regola');
            $table->integer('priorita')->default(50);
            $table->boolean('attiva')->default(true);
            
            // Condizioni
            $table->text('pattern_descrizione')->nullable();
            $table->text('pattern_controparte')->nullable();
            $table->text('pattern_causale')->nullable();
            $table->decimal('importo_min', 12, 2)->nullable();
            $table->decimal('importo_max', 12, 2)->nullable();
            $table->string('tipo_movimento_filtro', 50)->nullable();
            
            // Azioni
            $table->foreignId('piano_conto_target_id')->nullable()->constrained('piano_conti')->onDelete('set null');
            $table->string('categoria_movimento_auto', 50)->nullable();
            $table->string('descrizione_standardizzata', 500)->nullable();
            $table->boolean('auto_approva')->default(false);
            
            // Pattern Recognition
            $table->json('keywords_positive')->nullable();
            $table->json('keywords_negative')->nullable();
            $table->decimal('peso_matching', 5, 2)->default(10.00);
            
            // Sistema
            $table->text('note')->nullable();
            $table->timestamps();
            $table->foreignId('created_by')->nullable()->constrained('users')->onDelete('set null');
            $table->foreignId('updated_by')->nullable()->constrained('users')->onDelete('set null');
            
            // Indici
            $table->index(['gestione_id']);
            $table->index(['priorita']);
            $table->index(['attiva']);
        });
        
        // Trigger per UUID automatico
        DB::unprepared('
            CREATE TRIGGER trg_movimenti_bancari_uuid 
            BEFORE INSERT ON movimenti_bancari 
            FOR EACH ROW 
            BEGIN 
                IF NEW.uuid IS NULL OR NEW.uuid = "" THEN 
                    SET NEW.uuid = UUID(); 
                END IF; 
            END
        ');
        
        // Trigger per hash anti-duplicati
        DB::unprepared('
            CREATE TRIGGER trg_movimenti_bancari_hash 
            BEFORE INSERT ON movimenti_bancari 
            FOR EACH ROW 
            BEGIN 
                IF NEW.hash_import IS NULL THEN 
                    SET NEW.hash_import = SHA2(CONCAT(
                        NEW.gestione_id, "|",
                        NEW.conto_corrente_iban, "|",
                        NEW.data_operazione, "|",
                        NEW.data_valuta, "|",
                        NEW.importo, "|",
                        IFNULL(NEW.numero_operazione, ""), "|",
                        LEFT(NEW.descrizione_banca, 100)
                    ), 256);
                END IF; 
            END
        ');
    }

    public function down()
    {
        DB::unprepared('DROP TRIGGER IF EXISTS trg_movimenti_bancari_uuid');
        DB::unprepared('DROP TRIGGER IF EXISTS trg_movimenti_bancari_hash');
        
        Schema::dropIfExists('regole_matching_bancario');
        Schema::dropIfExists('conti_bancari_configurazione');
        Schema::dropIfExists('movimenti_bancari');
    }
};

🏗️ ELOQUENT MODEL

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use App\Traits\MultiTenantTrait;
use Carbon\Carbon;

/**
 * Movimento Bancario Model
 * 
 * Gestisce movimenti bancari con riconciliazione automatica
 */
class MovimentoBancario extends Model
{
    use HasFactory, MultiTenantTrait;

    protected $table = 'movimenti_bancari';

    protected $fillable = [
        'uuid',
        'gestione_id',
        'banca_codice_abi',
        'banca_denominazione', 
        'conto_corrente_iban',
        'conto_corrente_numero',
        'data_operazione',
        'data_valuta',
        'numero_operazione',
        'codice_operazione',
        'importo',
        'importo_commissioni',
        'divisa',
        'cambio',
        'descrizione_banca',
        'descrizione_normalizzata',
        'causale',
        'riferimento_operazione',
        'controparte_nome',
        'controparte_iban',
        'controparte_bic',
        'controparte_normalizzata',
        'tipo_movimento',
        'categoria_movimento',
        'fonte_import',
        'file_import',
        'hash_import',
        'data_import',
        'stato_riconciliazione',
        'contabilita_id',
        'matching_score',
        'riconciliato_da',
        'riconciliato_il',
        'note_riconciliazione',
        'unita_immobiliare_id',
        'soggetto_id',
        'rata_numero',
        'periodo_riferimento',
        'anomalia',
        'tipo_anomalia',
        'richiede_intervento',
        'sospeso_motivo',
        'pattern_riconosciuto',
        'confidence_level',
        'keywords_matched',
        'stato',
        'note',
        'created_by',
        'updated_by'
    ];

    protected $casts = [
        'data_operazione' => 'date',
        'data_valuta' => 'date',
        'data_import' => 'datetime',
        'riconciliato_il' => 'datetime',
        'importo' => 'decimal:2',
        'importo_commissioni' => 'decimal:2',
        'cambio' => 'decimal:6',
        'matching_score' => 'decimal:2',
        'confidence_level' => 'decimal:2',
        'keywords_matched' => 'array',
        'anomalia' => 'boolean',
        'richiede_intervento' => 'boolean',
        'rata_numero' => 'integer'
    ];

    // ==================== RELATIONSHIPS ====================

    public function gestione(): BelongsTo
    {
        return $this->belongsTo(Gestione::class);
    }

    public function contabilita(): BelongsTo
    {
        return $this->belongsTo(Contabilita::class);
    }

    public function unitaImmobiliare(): BelongsTo
    {
        return $this->belongsTo(UnitaImmobiliare::class);
    }

    public function soggetto(): BelongsTo
    {
        return $this->belongsTo(Soggetto::class);
    }

    public function riconciliatoDa(): BelongsTo
    {
        return $this->belongsTo(User::class, 'riconciliato_da');
    }

    public function createdBy(): BelongsTo
    {
        return $this->belongsTo(User::class, 'created_by');
    }

    public function updatedBy(): BelongsTo
    {
        return $this->belongsTo(User::class, 'updated_by');
    }

    // ==================== SCOPES ====================

    public function scopeNonRiconciliati($query)
    {
        return $query->where('stato_riconciliazione', 'NON_RICONCILIATO');
    }

    public function scopeRiconciliati($query)
    {
        return $query->where('stato_riconciliazione', 'RICONCILIATO');
    }

    public function scopeConAnomazie($query)
    {
        return $query->where('anomalia', true);
    }

    public function scopeEntrate($query)
    {
        return $query->where('tipo_movimento', 'ENTRATA');
    }

    public function scopeUscite($query)
    {
        return $query->where('tipo_movimento', 'USCITA');
    }

    public function scopePeriodo($query, Carbon $inizio, Carbon $fine)
    {
        return $query->whereBetween('data_operazione', [$inizio, $fine]);
    }

    public function scopeConto($query, string $iban)
    {
        return $query->where('conto_corrente_iban', $iban);
    }

    // ==================== BUSINESS METHODS ====================

    /**
     * Riconcilia movimento con registrazione contabile
     */
    public function riconcilia(int $contabilitaId, ?string $note = null): bool
    {
        $contabilita = Contabilita::find($contabilitaId);
        
        if (!$contabilita || $contabilita->gestione_id !== $this->gestione_id) {
            return false;
        }
        
        // Verifica coerenza importi (tolleranza 0.01€)
        $differenzaImporti = abs(abs($this->importo) - $contabilita->importo_totale);
        if ($differenzaImporti > 0.01) {
            return false;
        }
        
        $this->update([
            'stato_riconciliazione' => 'RICONCILIATO',
            'contabilita_id' => $contabilitaId,
            'riconciliato_da' => auth()->id(),
            'riconciliato_il' => now(),
            'note_riconciliazione' => $note,
            'matching_score' => 100.00,
            'anomalia' => false,
            'updated_by' => auth()->id()
        ]);
        
        // Aggiorna anche la contabilità
        $contabilita->update([
            'riconciliato' => true,
            'data_riconciliazione' => now(),
            'updated_by' => auth()->id()
        ]);
        
        return true;
    }

    /**
     * Rimuovi riconciliazione
     */
    public function annullaRiconciliazione(?string $motivo = null): bool
    {
        if ($this->stato_riconciliazione !== 'RICONCILIATO') {
            return false;
        }
        
        $contabilita = $this->contabilita;
        
        $this->update([
            'stato_riconciliazione' => 'NON_RICONCILIATO',
            'contabilita_id' => null,
            'riconciliato_da' => null,
            'riconciliato_il' => null,
            'note_riconciliazione' => $motivo,
            'matching_score' => null,
            'updated_by' => auth()->id()
        ]);
        
        // Aggiorna contabilità
        if ($contabilita) {
            $contabilita->update([
                'riconciliato' => false,
                'data_riconciliazione' => null,
                'updated_by' => auth()->id()
            ]);
        }
        
        return true;
    }

    /**
     * Calcola score di matching con registrazione contabile
     */
    public function calcolaMatchingScore(Contabilita $contabilita): float
    {
        $score = 0;
        
        // Verifica importo (40 punti max)
        $differenzaImporti = abs(abs($this->importo) - $contabilita->importo_totale);
        if ($differenzaImporti <= 0.01) {
            $score += 40;
        } elseif ($differenzaImporti <= 1) {
            $score += 30;
        } elseif ($differenzaImporti <= 10) {
            $score += 20;
        }
        
        // Verifica data (20 punti max)
        $differenzaGiorni = abs($this->data_operazione->diffInDays($contabilita->data_registrazione));
        if ($differenzaGiorni <= 1) {
            $score += 20;
        } elseif ($differenzaGiorni <= 7) {
            $score += 15;
        } elseif ($differenzaGiorni <= 30) {
            $score += 10;
        }
        
        // Verifica descrizione (20 punti max)
        $similaritaDescrizione = $this->calcolaSimilaritaDescrizione($contabilita->descrizione);
        $score += ($similaritaDescrizione / 100) * 20;
        
        // Verifica soggetto/unità (20 punti max)
        if ($this->soggetto_id && $contabilita->soggetto_id === $this->soggetto_id) {
            $score += 15;
        }
        if ($this->unita_immobiliare_id && $contabilita->unita_immobiliare_id === $this->unita_immobiliare_id) {
            $score += 15;
        }
        
        return min(100, $score);
    }

    /**
     * Calcola similarità tra descrizioni
     */
    private function calcolaSimilaritaDescrizione(string $descrizioneContabilita): float
    {
        $desc1 = $this->normalizzaDescrizione($this->descrizione_banca);
        $desc2 = $this->normalizzaDescrizione($descrizioneContabilita);
        
        // Algoritmo semplice di similarità basato su parole comuni
        $parole1 = explode(' ', $desc1);
        $parole2 = explode(' ', $desc2);
        
        $parolaComuni = array_intersect($parole1, $parole2);
        $totaleParole = array_unique(array_merge($parole1, $parole2));
        
        return count($totaleParole) > 0 ? (count($parolaComuni) / count($totaleParole)) * 100 : 0;
    }

    /**
     * Normalizza descrizione per matching
     */
    public function normalizzaDescrizione(string $descrizione): string
    {
        // Converti in minuscolo
        $normalized = strtolower($descrizione);
        
        // Rimuovi caratteri speciali
        $normalized = preg_replace('/[^a-z0-9\s]/', ' ', $normalized);
        
        // Rimuovi spazi multipli
        $normalized = preg_replace('/\s+/', ' ', $normalized);
        
        // Rimuovi parole comuni (stop words)
        $stopWords = ['di', 'da', 'per', 'con', 'del', 'della', 'dei', 'delle', 'il', 'la', 'lo', 'le', 'gli', 'un', 'una', 'uno'];
        $parole = explode(' ', $normalized);
        $paroleFiltrate = array_diff($parole, $stopWords);
        
        return trim(implode(' ', $paroleFiltrate));
    }

    /**
     * Analizza movimento per pattern recognition
     */
    public function analizzaPattern(): array
    {
        $patterns = [];
        $keywords = [];
        
        $descrizione = $this->normalizzaDescrizione($this->descrizione_banca);
        $controparte = $this->controparte_nome ? $this->normalizzaDescrizione($this->controparte_nome) : '';
        
        // Pattern Quote Condominiali
        if (preg_match('/quota|contributo|condomin/', $descrizione) || 
            ($this->tipo_movimento === 'ENTRATA' && $this->importo > 50)) {
            $patterns[] = 'QUOTA_CONDOMINIALE';
            $keywords[] = 'quota condominiale';
        }
        
        // Pattern Bonifico SEPA
        if (preg_match('/bonifico|sepa|transfer/', $descrizione)) {
            $patterns[] = 'BONIFICO_SEPA';
            $keywords[] = 'bonifico';
        }
        
        // Pattern Fornitore
        if ($this->tipo_movimento === 'USCITA' && $controparte && 
            preg_match('/spa|srl|snc|sas|ditta/', $controparte)) {
            $patterns[] = 'PAGAMENTO_FORNITORE';
            $keywords[] = 'fornitore';
        }
        
        // Pattern Commissioni
        if (preg_match('/commissione|spese|addebito/', $descrizione) && 
            $this->importo < 0 && abs($this->importo) < 50) {
            $patterns[] = 'COMMISSIONE_BANCARIA';
            $keywords[] = 'commissione';
        }
        
        // Aggiorna movimento con pattern riconosciuti
        if (!empty($patterns)) {
            $this->update([
                'pattern_riconosciuto' => implode(',', $patterns),
                'keywords_matched' => $keywords,
                'confidence_level' => $this->calcolaConfidenceLevel($patterns),
                'updated_by' => auth()->id()
            ]);
        }
        
        return [
            'patterns' => $patterns,
            'keywords' => $keywords,
            'confidence' => $this->confidence_level ?? 0
        ];
    }

    /**
     * Calcola livello di confidenza del riconoscimento
     */
    private function calcolaConfidenceLevel(array $patterns): float
    {
        $baseConfidence = count($patterns) * 20; // 20 punti per pattern
        
        // Bonus per importi tipici
        if ($this->tipo_movimento === 'ENTRATA' && $this->importo >= 100 && $this->importo <= 5000) {
            $baseConfidence += 15;
        }
        
        // Bonus per IBAN conosciuti
        if ($this->controparte_iban && $this->soggetto_id) {
            $baseConfidence += 25;
        }
        
        // Bonus per dati coerenti
        if ($this->data_operazione->isWeekday()) { // Giorni lavorativi
            $baseConfidence += 10;
        }
        
        return min(100, $baseConfidence);
    }

    /**
     * Segna movimento come anomalo
     */
    public function segnalaAnomaila(string $tipoAnomalia, string $descrizione = null): void
    {
        $this->update([
            'anomalia' => true,
            'tipo_anomalia' => $tipoAnomalia,
            'richiede_intervento' => true,
            'sospeso_motivo' => $descrizione,
            'stato_riconciliazione' => 'SOSPESO',
            'updated_by' => auth()->id()
        ]);
    }

    /**
     * Risolvi anomalia
     */
    public function risolviAnomalia(string $note = null): void
    {
        $this->update([
            'anomalia' => false,
            'tipo_anomalia' => null,
            'richiede_intervento' => false,
            'sospeso_motivo' => null,
            'stato_riconciliazione' => 'NON_RICONCILIATO',
            'note_riconciliazione' => $note,
            'updated_by' => auth()->id()
        ]);
    }

    /**
     * Trova possibili match automatici
     */
    public function trovaMatchAutomatici(): array
    {
        $matches = [];
        
        // Cerca registrazioni contabili non riconciliate nello stesso periodo
        $registrazioni = Contabilita::where('gestione_id', $this->gestione_id)
            ->where('riconciliato', false)
            ->whereBetween('data_registrazione', [
                $this->data_operazione->subDays(7),
                $this->data_operazione->addDays(7)
            ])
            ->get();
        
        foreach ($registrazioni as $registrazione) {
            $score = $this->calcolaMatchingScore($registrazione);
            
            if ($score >= 60) { // Soglia minima per considerare match
                $matches[] = [
                    'contabilita_id' => $registrazione->id,
                    'score' => $score,
                    'descrizione' => $registrazione->descrizione,
                    'importo' => $registrazione->importo_totale,
                    'data' => $registrazione->data_registrazione,
                    'auto_approvabile' => $score >= 95
                ];
            }
        }
        
        // Ordina per score decrescente
        usort($matches, function($a, $b) {
            return $b['score'] <=> $a['score'];
        });
        
        return $matches;
    }

    // ==================== EVENTI MODEL ====================

    protected static function booted()
    {
        static::creating(function ($movimento) {
            // Genera UUID se non presente
            if (!$movimento->uuid) {
                $movimento->uuid = \Str::uuid();
            }
            
            // Normalizza descrizione per matching
            if ($movimento->descrizione_banca) {
                $movimento->descrizione_normalizzata = $movimento->normalizzaDescrizione($movimento->descrizione_banca);
            }
            
            // Normalizza nome controparte
            if ($movimento->controparte_nome) {
                $movimento->controparte_normalizzata = $movimento->normalizzaDescrizione($movimento->controparte_nome);
            }
            
            // Determina tipo movimento se non specificato
            if (!$movimento->tipo_movimento) {
                $movimento->tipo_movimento = $movimento->importo >= 0 ? 'ENTRATA' : 'USCITA';
            }
            
            $movimento->created_by = auth()->id();
        });
        
        static::updating(function ($movimento) {
            $movimento->updated_by = auth()->id();
        });
        
        static::created(function ($movimento) {
            // Analizza pattern automaticamente
            $movimento->analizzaPattern();
        });
        
        // Filtro multi-tenant
        static::addGlobalScope(new \App\Scopes\MultiTenantScope);
    }
}

🔄 SISTEMA DI IMPORT AUTOMATICO

Parser CBI (Corporate Banking Interface)

<?php

namespace App\Services;

/**
 * Service per import automatico movimenti bancari da file CBI
 */
class CbiImportService
{
    private $gestione;
    private $contoConfigurazione;
    
    public function __construct(Gestione $gestione, ContoBancarioConfigurazione $conto)
    {
        $this->gestione = $gestione;
        $this->contoConfigurazione = $conto;
    }
    
    /**
     * Importa movimenti da file CBI
     */
    public function importFile(string $filePath): array
    {
        $risultato = [
            'importati' => 0,
            'duplicati' => 0,
            'errori' => 0,
            'anomalie' => 0,
            'movimenti' => []
        ];
        
        $contenuto = file_get_contents($filePath);
        $righe = explode("\n", $contenuto);
        
        foreach ($righe as $numeroRiga => $riga) {
            try {
                if (strlen(trim($riga)) === 0) continue;
                
                $movimento = $this->parseRigaCbi($riga, $numeroRiga);
                
                if ($movimento) {
                    $movimentoSalvato = $this->salvaMovimento($movimento, basename($filePath));
                    
                    if ($movimentoSalvato) {
                        $risultato['importati']++;
                        $risultato['movimenti'][] = $movimentoSalvato;
                        
                        // Analizza automaticamente per matching
                        $this->tentaMatchingAutomatico($movimentoSalvato);
                    }
                }
                
            } catch (\Exception $e) {
                $risultato['errori']++;
                \Log::error("Errore import CBI riga {$numeroRiga}: " . $e->getMessage());
            }
        }
        
        return $risultato;
    }
    
    /**
     * Parsing singola riga formato CBI
     */
    private function parseRigaCbi(string $riga, int $numeroRiga): ?array
    {
        // Formato CBI standard: posizioni fisse
        if (strlen($riga) < 120) {
            return null;
        }
        
        $data = [
            'data_operazione' => $this->parseCbiDate(substr($riga, 40, 6)),
            'data_valuta' => $this->parseCbiDate(substr($riga, 46, 6)),
            'importo' => $this->parseCbiAmount(substr($riga, 52, 15), substr($riga, 67, 1)),
            'numero_operazione' => trim(substr($riga, 10, 10)),
            'codice_operazione' => trim(substr($riga, 20, 3)),
            'descrizione_banca' => trim(substr($riga, 80, 40)),
            'riferimento_operazione' => trim(substr($riga, 120, 16)),
        ];
        
        // Campi aggiuntivi se presenti
        if (strlen($riga) > 140) {
            $data['controparte_nome'] = trim(substr($riga, 140, 30));
            $data['controparte_iban'] = trim(substr($riga, 170, 34));
        }
        
        return $data;
    }
    
    /**
     * Parse data formato CBI (GGMMAA)
     */
    private function parseCbiDate(string $dateString): ?\Carbon\Carbon
    {
        if (strlen($dateString) !== 6) return null;
        
        $giorno = substr($dateString, 0, 2);
        $mese = substr($dateString, 2, 2);
        $anno = '20' . substr($dateString, 4, 2);
        
        try {
            return \Carbon\Carbon::createFromFormat('Y-m-d', "{$anno}-{$mese}-{$giorno}");
        } catch (\Exception $e) {
            return null;
        }
    }
    
    /**
     * Parse importo CBI (15 cifre + segno)
     */
    private function parseCbiAmount(string $amountString, string $signFlag): float
    {
        $amount = (float) $amountString / 100; // Ultimi 2 cifre sono centesimi
        return $signFlag === 'D' ? -$amount : $amount; // D=Dare (uscita), A=Avere (entrata)
    }
    
    /**
     * Salva movimento nel database
     */
    private function salvaMovimento(array $datiMovimento, string $nomeFile): ?MovimentoBancario
    {
        // Completa dati movimento
        $datiCompleti = array_merge($datiMovimento, [
            'gestione_id' => $this->gestione->id,
            'banca_codice_abi' => $this->contoConfigurazione->banca_abi,
            'banca_denominazione' => $this->contoConfigurazione->banca_denominazione,
            'conto_corrente_iban' => $this->contoConfigurazione->iban,
            'fonte_import' => 'CBI',
            'file_import' => $nomeFile,
            'data_import' => now(),
            'tipo_movimento' => $datiMovimento['importo'] >= 0 ? 'ENTRATA' : 'USCITA',
            'stato_riconciliazione' => 'NON_RICONCILIATO'
        ]);
        
        // Verifica duplicati tramite hash
        $hashMovimento = $this->generaHashMovimento($datiCompleti);
        
        if (MovimentoBancario::where('hash_import', $hashMovimento)->exists()) {
            return null; // Duplicato
        }
        
        $datiCompleti['hash_import'] = $hashMovimento;
        
        return MovimentoBancario::create($datiCompleti);
    }
    
    /**
     * Genera hash per prevenire duplicati
     */
    private function generaHashMovimento(array $dati): string
    {
        $stringaHash = implode('|', [
            $dati['gestione_id'],
            $dati['conto_corrente_iban'],
            $dati['data_operazione']->format('Y-m-d'),
            $dati['data_valuta']->format('Y-m-d'),
            $dati['importo'],
            $dati['numero_operazione'] ?? '',
            substr($dati['descrizione_banca'], 0, 100)
        ]);
        
        return hash('sha256', $stringaHash);
    }
    
    /**
     * Tenta matching automatico dopo import
     */
    private function tentaMatchingAutomatico(MovimentoBancario $movimento): void
    {
        $matches = $movimento->trovaMatchAutomatici();
        
        foreach ($matches as $match) {
            if ($match['auto_approvabile'] && $match['score'] >= $this->contoConfigurazione->soglia_auto_riconciliazione) {
                // Auto-riconcilia se score è molto alto
                if ($this->contoConfigurazione->auto_riconcilia_perfetti) {
                    $movimento->riconcilia(
                        $match['contabilita_id'],
                        "Riconciliazione automatica (score: {$match['score']})"
                    );
                    break;
                }
            }
        }
        
        // Se non riconciliato automaticamente ma con anomalie, segna
        if ($movimento->stato_riconciliazione === 'NON_RICONCILIATO') {
            $this->verificaAnomalieMovimento($movimento);
        }
    }
    
    /**
     * Verifica anomalie nel movimento importato
     */
    private function verificaAnomalieMovimento(MovimentoBancario $movimento): void
    {
        // Importo sospetto (troppo alto o troppo basso)
        if (abs($movimento->importo) > 50000) {
            $movimento->segnalaAnomaila('IMPORTO_ELEVATO', 'Importo superiore a 50.000€');
            return;
        }
        
        if (abs($movimento->importo) < 0.01) {
            $movimento->segnalaAnomaila('IMPORTO_ZERO', 'Importo troppo basso');
            return;
        }
        
        // Data futura
        if ($movimento->data_operazione->isFuture()) {
            $movimento->segnalaAnomaila('DATA_FUTURA', 'Data operazione nel futuro');
            return;
        }
        
        // Weekend per operazioni sospette
        if ($movimento->data_operazione->isWeekend() && $movimento->tipo_movimento === 'ENTRATA' && $movimento->importo > 1000) {
            $movimento->segnalaAnomaila('OPERAZIONE_WEEKEND', 'Grande entrata in weekend');
            return;
        }
        
        // Descrizione vuota o sospetta
        if (empty(trim($movimento->descrizione_banca)) || strlen(trim($movimento->descrizione_banca)) < 5) {
            $movimento->segnalaAnomaila('DESCRIZIONE_INSUFFICIENTE', 'Descrizione troppo breve o vuota');
            return;
        }
    }
}

📊 DASHBOARD RICONCILIAZIONE

Query per Dashboard

-- RIEPILOGO RICONCILIAZIONE PER GESTIONE
SELECT 
    g.denominazione AS gestione,
    cbc.denominazione AS conto,
    COUNT(*) AS totale_movimenti,
    COUNT(CASE WHEN mb.stato_riconciliazione = 'RICONCILIATO' THEN 1 END) AS riconciliati,
    COUNT(CASE WHEN mb.stato_riconciliazione = 'NON_RICONCILIATO' THEN 1 END) AS da_riconciliare,
    COUNT(CASE WHEN mb.anomalia = 1 THEN 1 END) AS con_anomalie,
    SUM(CASE WHEN mb.tipo_movimento = 'ENTRATA' THEN mb.importo ELSE 0 END) AS totale_entrate,
    SUM(CASE WHEN mb.tipo_movimento = 'USCITA' THEN ABS(mb.importo) ELSE 0 END) AS totale_uscite,
    SUM(mb.importo) AS saldo_periodo
FROM movimenti_bancari mb
INNER JOIN gestioni g ON mb.gestione_id = g.id
LEFT JOIN conti_bancari_configurazione cbc ON mb.conto_corrente_iban = cbc.iban AND mb.gestione_id = cbc.gestione_id
WHERE mb.data_operazione BETWEEN :data_inizio AND :data_fine
  AND mb.stato = 'ATTIVO'
  AND mb.gestione_id = :gestione_id
GROUP BY g.id, cbc.id
ORDER BY g.denominazione, cbc.denominazione;

-- MOVIMENTI NON RICONCILIATI CON SUGGERIMENTI
SELECT 
    mb.*,
    c.id AS contabilita_suggerita_id,
    c.descrizione AS contabilita_descrizione,
    c.importo_totale AS contabilita_importo,
    c.data_registrazione AS contabilita_data,
    ABS(mb.importo - c.importo_totale) AS differenza_importo,
    DATEDIFF(mb.data_operazione, c.data_registrazione) AS differenza_giorni
FROM movimenti_bancari mb
LEFT JOIN contabilita c ON (
    c.gestione_id = mb.gestione_id 
    AND c.riconciliato = FALSE
    AND ABS(ABS(mb.importo) - c.importo_totale) <= 1.00
    AND ABS(DATEDIFF(mb.data_operazione, c.data_registrazione)) <= 7
)
WHERE mb.stato_riconciliazione = 'NON_RICONCILIATO'
  AND mb.gestione_id = :gestione_id
  AND mb.anomalia = FALSE
ORDER BY mb.data_operazione DESC, ABS(mb.importo - IFNULL(c.importo_totale, 0));

🚀 PROSSIMI STEP

Completato: Movimenti Bancari con riconciliazione automatica
🔄 Prossimo: Fatture Elettroniche XML per compliance fiscale
Seguente: Testing e implementazione

Il sistema Movimenti Bancari è completo con:

  • 🔄 Import automatico CBI/SEPA
  • 🎯 Matching intelligente con score
  • Riconciliazione automatica sopra soglia
  • 🚨 Gestione anomalie e sospesi

Procediamo con le Fatture Elettroniche? 📄


🧠 SISTEMA RICONOSCIMENTO AUTOMATICO DATI REALI

📊 PATTERN IDENTIFICATI DA UNICREDIT

Dall'analisi dei movimenti bancari reali di UNICREDIT (Supercondominio Viale delle Milizie 3) sono stati identificati pattern molto precisi per il riconoscimento automatico:

Esempi movimenti reali analizzati:

  • BONIFICO A VOSTRO FAVORE BONIFICO SEPA DA DUCA CARLO PER SALDO RISC. 22-23 B-1 COMM 0,00 SPESE 0,00 TRN 1001240021013258
  • BONIFICO A VOSTRO FAVORE BONIFICO SEPA DA ROSSI MARIO PER quota cond 2024 B/8 avviso 13675 prima rata
  • DISPOSIZIONE DI BONIFICO SEPA A ENEL ENERGIA SPA PER SALDO FATT 12345

🏗️ TABELLE AGGIUNTIVE PER RICONOSCIMENTO AVANZATO

-- Tabella pattern riconoscimento personalizzabili per banca
CREATE TABLE movimenti_bancari_pattern (
    -- Identificativi
    id                          BIGINT PRIMARY KEY AUTO_INCREMENT,
    nome_pattern                VARCHAR(100) NOT NULL,                  -- Nome identificativo pattern
    banca_abi                   VARCHAR(5) NULL,                        -- Specifico per banca (NULL = tutti)
    
    -- Pattern Regex
    regex_descrizione           TEXT NOT NULL,                          -- Regex per matching descrizione
    regex_flags                 VARCHAR(10) DEFAULT 'i',                -- Flags regex (i=insensitive, etc.)
    
    -- Estrazione Dati
    campo_estratto              VARCHAR(50) NOT NULL,                   -- Campo da estrarre
    regex_estrazione            TEXT NULL,                              -- Regex per estrazione valore
    valore_default              VARCHAR(100) NULL,                      -- Valore default se non estratto
    
    -- Mapping Valori
    mapping_valori              JSON NULL,                              -- Mapping valori estratti
    
    -- Classificazione
    tipo_movimento_target       ENUM('ENTRATA','USCITA','GIROCONTO','COMMISSIONE','INTERESSE') NULL,
    categoria_target            VARCHAR(50) NULL,                       -- Categoria da assegnare
    priorita                    INT DEFAULT 100,                        -- Priorità applicazione (1=max)
    
    -- Configurazione
    attivo                      BOOLEAN DEFAULT TRUE,
    confidence_score            DECIMAL(3,2) DEFAULT 0.8,               -- Score confidenza pattern
    
    -- Sistema
    created_at                  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at                  TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    -- Indici
    INDEX idx_banca_abi (banca_abi),
    INDEX idx_attivo (attivo),
    INDEX idx_priorita (priorita)
);

-- Log riconoscimento per debug e miglioramento
CREATE TABLE movimenti_bancari_riconoscimento_log (
    id                          BIGINT PRIMARY KEY AUTO_INCREMENT,
    movimento_bancario_id       BIGINT NOT NULL,                        -- FK movimenti_bancari.id
    pattern_id                  BIGINT NOT NULL,                        -- FK movimenti_bancari_pattern.id
    
    -- Risultato Matching
    matched                     BOOLEAN NOT NULL,                       -- Se pattern ha fatto match
    valore_estratto             TEXT NULL,                              -- Valore estratto dal pattern
    confidence_score            DECIMAL(3,2) NOT NULL,                  -- Score confidenza
    
    -- Debug
    regex_utilizzata            TEXT NOT NULL,                          -- Regex utilizzata
    testo_analizzato            TEXT NOT NULL,                          -- Testo su cui è stata applicata
    
    -- Sistema
    created_at                  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    
    -- Indici
    INDEX idx_movimento (movimento_bancario_id),
    INDEX idx_pattern (pattern_id),
    INDEX idx_matched (matched),
    
    -- Foreign Keys
    FOREIGN KEY (movimento_bancario_id) REFERENCES movimenti_bancari(id) ON DELETE CASCADE,
    FOREIGN KEY (pattern_id) REFERENCES movimenti_bancari_pattern(id) ON DELETE CASCADE
);

🎯 CAMPI AGGIUNTIVI NELLA TABELLA PRINCIPALE

-- Aggiunta campi riconoscimento automatico alla tabella movimenti_bancari
ALTER TABLE movimenti_bancari ADD COLUMN causale_bancaria_codice VARCHAR(10) NULL COMMENT 'Codice causale bancaria (048, 208, etc.)';
ALTER TABLE movimenti_bancari ADD COLUMN unita_immobiliare_estratta VARCHAR(20) NULL COMMENT 'UI estratta automaticamente (B-1, C4, etc.)';
ALTER TABLE movimenti_bancari ADD COLUMN scala_estratta VARCHAR(5) NULL COMMENT 'Scala estratta (A, B, C, D)';
ALTER TABLE movimenti_bancari ADD COLUMN interno_estratto VARCHAR(10) NULL COMMENT 'Numero interno estratto';
ALTER TABLE movimenti_bancari ADD COLUMN avviso_pagamento_numero VARCHAR(20) NULL COMMENT 'Numero avviso di pagamento';
ALTER TABLE movimenti_bancari ADD COLUMN rata_numero INT NULL COMMENT 'Numero rata (1, 2, 3, etc.)';
ALTER TABLE movimenti_bancari ADD COLUMN rata_tipologia VARCHAR(50) NULL COMMENT 'Tipologia rata (RISCALDAMENTO, CONDOMINIO, etc.)';
ALTER TABLE movimenti_bancari ADD COLUMN anno_competenza YEAR NULL COMMENT 'Anno di competenza estratto';
ALTER TABLE movimenti_bancari ADD COLUMN periodo_riferimento VARCHAR(20) NULL COMMENT 'Periodo riferimento (22-23, 2023-2024, etc.)';
ALTER TABLE movimenti_bancari ADD COLUMN riferimento_fattura VARCHAR(50) NULL COMMENT 'Numero fattura se presente';

-- Campi specifici INTESA SANPAOLO
ALTER TABLE movimenti_bancari ADD COLUMN codice_disposizione_banca VARCHAR(20) NULL COMMENT 'Codice disposizione banca (es: 0124010543443092)';
ALTER TABLE movimenti_bancari ADD COLUMN riferimento_polizza VARCHAR(50) NULL COMMENT 'Numero polizza assicurativa se presente';
ALTER TABLE movimenti_bancari ADD COLUMN bic_ordinante VARCHAR(20) NULL COMMENT 'BIC della banca ordinante (es: INGBITD1)';

-- Indici per ricerca veloce
ALTER TABLE movimenti_bancari ADD INDEX idx_unita_estratta (unita_immobiliare_estratta);
ALTER TABLE movimenti_bancari ADD INDEX idx_avviso_numero (avviso_pagamento_numero);
ALTER TABLE movimenti_bancari ADD INDEX idx_rata_tipologia (rata_tipologia);
ALTER TABLE movimenti_bancari ADD INDEX idx_anno_competenza (anno_competenza);
ALTER TABLE movimenti_bancari ADD INDEX idx_codice_disposizione (codice_disposizione_banca);
ALTER TABLE movimenti_bancari ADD INDEX idx_scala_interno (scala_estratta, interno_estratto);

🚀 VANTAGGI SISTEMA RICONOSCIMENTO

Riconoscimento Automatico: Estrazione automatica unità immobiliari, rate, periodi
Pattern Configurabili: Aggiunta nuove banche senza modifiche codice
Dati Reali: Basato su analisi movimenti bancari effettivi UNICREDIT
Debug Completo: Log dettagliato per miglioramento continuo
Multi-Banca: Supporto pattern specifici per ogni banca
Confidence Score: Valutazione affidabilità riconoscimento
Matching Intelligente: Collegamento automatico pagamenti-unità immobiliari

Il sistema potrà automaticamente collegare i pagamenti alle unità immobiliari e ai condomini corrispondenti, facilitando enormemente la riconciliazione contabile.

Ready per analizzare altre banche: Sistema pronto per espandere i pattern con dati di BNL, Intesa Sanpaolo, Banca Mediolanum, ecc.


🏦 PATTERN INTESA SANPAOLO IDENTIFICATI

📊 ANALISI DATI REALI INTESA SANPAOLO

Dall'analisi dei movimenti bancari reali di INTESA SANPAOLO (Condominio Via Andrea Doria 36) sono emersi pattern molto specifici e diversi da UNICREDIT:

Esempi movimenti reali analizzati:

  • ACCREDITO BEU CON CONTABILE;218,00;;COD. DISP.: 0124010543443092 CASH Ordinaria provvisoria Gen Marz 2024 Severi via Andrea Doria scala B int 10
  • ACCREDITO BEU CON CONTABILE;499,03;;COD. DISP.: 0124011053312441 CASH Condominio come da avviso 0007-0018/2024 Rata 2 straord lavori cortile e ord provvisori gen-mar 2024 - Coppari int 10-11
  • BONIFICO IN EURO VERSO UE/SEPA CANALE TELEM.;;-1003,78;0124010847939613 20240108-EQhkS8okdafsMZcan3hejJYXLT Bonifico da Voi disposto a favore di: UNIPOLSAI ASSICURAZIONI SPA Saldo ft. Polizza del 08/01/24

🎯 PATTERN SPECIFICI INTESA SANPAOLO

-- Pattern INTESA SANPAOLO basati su dati reali Condominio Via Andrea Doria 36
INSERT INTO movimenti_bancari_pattern (nome_pattern, banca_abi, regex_descrizione, campo_estratto, regex_estrazione, tipo_movimento_target, categoria_target, priorita) VALUES

-- ENTRATE CONDOMINI (ACCREDITO BEU CON CONTABILE)
('INTESA - Accredito BEU', '03069', 'ACCREDITO BEU CON CONTABILE', 'tipo_movimento', NULL, 'ENTRATA', 'INCASSO_CONDOMINIO', 10),

-- CODICE DISPOSIZIONE (unico per ogni transazione)
('INTESA - Codice Disposizione', '03069', 'COD\\. DISP\\.: (\\d+)', 'codice_disposizione_banca', 'COD\\. DISP\\.: (\\d+)', 'ENTRATA', NULL, 15),

-- UNITÀ IMMOBILIARI (pattern specifici Intesa)
('INTESA - Scala Int Format', '03069', 'scala\\s+([A-D])\\s+int\\s+(\\d+)', 'scala_interno', 'scala\\s+([A-D])\\s+int\\s+(\\d+)', 'ENTRATA', 'QUOTA_CONDOMINIALE', 20),
('INTESA - Int Format', '03069', '\\bint\\s+([a-d]?\\d+)', 'interno_estratto', '\\bint\\s+([a-d]?\\d+)', 'ENTRATA', 'QUOTA_CONDOMINIALE', 21),
('INTESA - Appartamento Format', '03069', 'APPARTAMENTO\\s+([A-D]\\d+)', 'unita_immobiliare_estratta', 'APPARTAMENTO\\s+([A-D]\\d+)', 'ENTRATA', 'QUOTA_CONDOMINIALE', 22),
('INTESA - Int Bis Format', '03069', 'interno\\s+(\\d+\\s*bis)', 'interno_estratto', 'interno\\s+(\\d+\\s*bis)', 'ENTRATA', 'QUOTA_CONDOMINIALE', 23),

-- MITTENTI (estrazione automatica da campo MITT.)
('INTESA - Mittente', '03069', 'MITT\\.: ([^B]+)\\s+BENEF\\.:?', 'controparte_nome', 'MITT\\.: ([^B]+)\\s+BENEF\\.:?', 'ENTRATA', 'QUOTA_CONDOMINIALE', 25),

-- AVVISI PAGAMENTO (formato specifico Intesa)
('INTESA - Avviso Completo', '03069', 'avviso\\s+(\\d{4})[-/](\\d{4})/(\\d{4})', 'avviso_pagamento_numero', 'avviso\\s+(\\d{4})[-/](\\d{4})/(\\d{4})', 'ENTRATA', 'QUOTA_CONDOMINIALE', 30),
('INTESA - Avviso Semplice', '03069', 'AVVISO\\s+DI\\s+PAGAMENTO\\s+(\\d+)', 'avviso_pagamento_numero', 'AVVISO\\s+DI\\s+PAGAMENTO\\s+(\\d+)', 'ENTRATA', 'QUOTA_CONDOMINIALE', 31),

-- PERIODI E RATE (molto specifici per Intesa)
('INTESA - Periodo Gen Mar', '03069', '(gennaio|gen)\\s*[-/]?\\s*(marzo|mar)\\s*20(\\d{2})', 'periodo_riferimento', '(gennaio|gen)\\s*[-/]?\\s*(marzo|mar)\\s*20(\\d{2})', 'ENTRATA', 'QUOTA_CONDOMINIALE', 40),
('INTESA - Periodo Apr Giu', '03069', '(aprile|apr)\\s*[-/]?\\s*(giugno|giu)\\s*20(\\d{2})', 'periodo_riferimento', '(aprile|apr)\\s*[-/]?\\s*(giugno|giu)\\s*20(\\d{2})', 'ENTRATA', 'QUOTA_CONDOMINIALE', 40),
('INTESA - Periodo Ott Dic', '03069', '(ottobre|ott)\\s*[-/]?\\s*(dicembre|dic)\\s*20(\\d{2})', 'periodo_riferimento', '(ottobre|ott)\\s*[-/]?\\s*(dicembre|dic)\\s*20(\\d{2})', 'ENTRATA', 'QUOTA_CONDOMINIALE', 40),

-- TIPOLOGIE SPECIFICHE INTESA
('INTESA - Ordinaria Provvisoria', '03069', '(ordinaria|ord)\\s+(provvisoria|provv)', 'rata_tipologia', NULL, 'ENTRATA', 'QUOTA_ORDINARIA_PROVVISORIA', 45),
('INTESA - Straordinaria Lavori', '03069', '(straordinaria|straord)\\s+lavori', 'rata_tipologia', NULL, 'ENTRATA', 'QUOTA_STRAORDINARIA_LAVORI', 45),
('INTESA - Lavori Cortile', '03069', 'lavori\\s+cortile', 'rata_tipologia', NULL, 'ENTRATA', 'QUOTA_LAVORI_CORTILE', 46),
('INTESA - Level One', '03069', 'LEVEL\\s*ONE|LEVELONE', 'rata_tipologia', NULL, 'ENTRATA', 'QUOTA_LEVELONE', 47),

-- NUMERO RATA (specifico Intesa)
('INTESA - Prima Rata', '03069', '\\b1\\s*RATA|prima\\s+rata|1\\s*di\\s*\\d+', 'rata_numero', '1', 'ENTRATA', 'QUOTA_CONDOMINIALE', 50),
('INTESA - Seconda Rata', '03069', '\\b2\\s*RATA|seconda\\s+rata|2\\s*di\\s*\\d+|2A\\s*RATA', 'rata_numero', '2', 'ENTRATA', 'QUOTA_CONDOMINIALE', 50),
('INTESA - Terza Rata', '03069', '\\b3\\s*RATA|terza\\s+rata|3\\s*di\\s*\\d+|3A\\s*RATA', 'rata_numero', '3', 'ENTRATA', 'QUOTA_CONDOMINIALE', 50),
('INTESA - Quarta Rata', '03069', '\\b4\\s*RATA|quarta\\s+rata|4\\s*di\\s*\\d+', 'rata_numero', '4', 'ENTRATA', 'QUOTA_CONDOMINIALE', 50),

-- ANNI COMPETENZA
('INTESA - Anno 2024', '03069', '\\b2024\\b', 'anno_competenza', '2024', 'ENTRATA', 'QUOTA_CONDOMINIALE', 60),
('INTESA - Anno 2023', '03069', '\\b2023\\b', 'anno_competenza', '2023', 'ENTRATA', 'QUOTA_CONDOMINIALE', 60),
('INTESA - Anno 2025', '03069', '\\b2025\\b', 'anno_competenza', '2025', 'ENTRATA', 'QUOTA_CONDOMINIALE', 60),

-- SPESE USCITE (BONIFICO IN EURO VERSO UE/SEPA)
('INTESA - Bonifico Uscita', '03069', 'BONIFICO IN EURO VERSO UE/SEPA', 'tipo_movimento', NULL, 'USCITA', 'PAGAMENTO_FORNITORE', 70),
('INTESA - Disposizione Bonifico', '03069', 'DISPOSIZIONE DI BONIFICO', 'tipo_movimento', NULL, 'USCITA', 'PAGAMENTO_FORNITORE', 71),

-- FATTURE (pattern specifico Intesa)
('INTESA - Saldo Fattura', '03069', 'Saldo\\s+ft\\.?\\s*([A-Z0-9/-]+)', 'riferimento_fattura', 'Saldo\\s+ft\\.?\\s*([A-Z0-9/-]+)', 'USCITA', 'PAGAMENTO_FATTURA', 75),
('INTESA - Saldo FT N', '03069', 'SALDO\\s+FT\\s+N\\.?\\s*([A-Z0-9/-]+)', 'riferimento_fattura', 'SALDO\\s+FT\\s+N\\.?\\s*([A-Z0-9/-]+)', 'USCITA', 'PAGAMENTO_FATTURA', 76),

-- POLIZZE E ASSICURAZIONI
('INTESA - Saldo Polizza', '03069', 'Saldo\\s+(Polizza|polizza)\\s*(\\d+)', 'riferimento_polizza', 'Saldo\\s+(Polizza|polizza)\\s*(\\d+)', 'USCITA', 'PAGAMENTO_ASSICURAZIONE', 80),

-- COMMISSIONI E SPESE (specifiche Intesa)
('INTESA - Costo Bonifico', '03069', 'COSTO PER BONIFICO', 'tipo_movimento', NULL, 'COMMISSIONE', 'COMMISSIONE_BONIFICO', 85),
('INTESA - Commissione Bonifico', '03069', 'COMMISSIONE.*BONIFICO', 'tipo_movimento', NULL, 'COMMISSIONE', 'COMMISSIONE_BONIFICO', 86),
('INTESA - Canone Fisso', '03069', 'CANONE FISSO MENSILE', 'tipo_movimento', NULL, 'COMMISSIONE', 'CANONE_BANCARIO', 87),
('INTESA - Canone Carta', '03069', 'CANONE CARTA DI DEBITO', 'tipo_movimento', NULL, 'COMMISSIONE', 'CANONE_CARTA', 88),
('INTESA - Imposta Bollo', '03069', 'IMPOSTA DI BOLLO', 'tipo_movimento', NULL, 'COMMISSIONE', 'IMPOSTA_BOLLO', 89),

-- F24 E TASSE
('INTESA - F24', '03069', 'PAGAMENTO.*F24|DELEGHE F24', 'tipo_movimento', NULL, 'USCITA', 'PAGAMENTO_TASSE', 90),

-- BONIFICO MYBANK (specifico Intesa)
('INTESA - MyBank', '03069', 'BONIFICO MYBANK', 'tipo_movimento', NULL, 'USCITA', 'PAGAMENTO_UTENZE', 91),
('INTESA - Commissione MyBank', '03069', 'COMMISSIONE BONIFICO MYBANK', 'tipo_movimento', NULL, 'COMMISSIONE', 'COMMISSIONE_MYBANK', 92),

-- BOLLETTINO POSTALE
('INTESA - Bollettino Postale', '03069', 'PAGAMENTO BOLLETTINO POSTALE', 'tipo_movimento', NULL, 'USCITA', 'PAGAMENTO_BOLLETTINO', 95),
('INTESA - Commissione Bollettino', '03069', 'COMMISSIONE PAGAMENTO BOLLETTINO', 'tipo_movimento', NULL, 'COMMISSIONE', 'COMMISSIONE_BOLLETTINO', 96);

🔄 ESEMPIO RICONOSCIMENTO INTESA

Movimento di esempio:

ACCREDITO BEU CON CONTABILE;218,00;;COD. DISP.: 0124010543443092 CASH Ordinaria provvisoria Gen Marz 2024 Severi via Andrea Doria scala B int 10 Bonifico a Vostro favore disposto da: MITT.: Renata Severi BENEF.: Condominio Via Andrea Doria 36 BIC. ORD.: INGBITD1

Dati estratti automaticamente:

  • codice_disposizione_banca: "0124010543443092"
  • scala_estratta: "B"
  • interno_estratto: "10"
  • controparte_nome: "Renata Severi"
  • rata_tipologia: "ORDINARIA_PROVVISORIA"
  • periodo_riferimento: "Gen Marz 2024"
  • anno_competenza: "2024"
  • tipo_movimento: "ENTRATA"
  • categoria_movimento: "QUOTA_ORDINARIA_PROVVISORIA"

🆚 CONFRONTO UNICREDIT vs INTESA

Caratteristica UNICREDIT INTESA SANPAOLO
Formato CSV ; separato, causale finale ; separato, descrizione estesa
Codice Operazione Causale numerica (048, 208) Tipo operazione testuale
Unità Immobiliari B-1, B/8, C4, A26 scala B int 10, APPARTAMENTO B04
Mittente Nel campo descrizione Campo specifico MITT.:
Codice Transazione TRN finale COD. DISP.: iniziale
Rate 1 RATA, prima rata 1 di 4, prima rata, 2A RATA
Periodi 22-23, 23-24 gennaio marzo 2024, gen-mar
Commissioni Integrate nella transazione Righe separate COSTO PER

🚀 VANTAGGI SISTEMA MULTI-BANCA

Pattern Specifici: Ogni banca ha i suoi pattern personalizzati
Riconoscimento Avanzato: Estrazione dati molto più dettagliata
Codici Operazione: Supporto sia causali numeriche che testuali
Flessibilità Formato: Gestione CSV diversi per ogni banca
Unità Immobiliari: Riconoscimento format diversi (B-1 vs scala B int 10)
Confidence Scoring: Valutazione qualità match per ogni banca

Il sistema ora riconosce automaticamente UNICREDIT e INTESA SANPAOLO con pattern specifici per ogni banca! 🏦

📊 RIEPILOGO ANALISI INTESA SANPAOLO

Dati Analizzati:

  • File: Movimenti_Conto_05082025.csv
  • Condominio: Via Andrea Doria 36
  • Periodo: 01/01/2024 - 05/08/2025
  • Totale movimenti: 444 operazioni
  • Saldo finale: 10.230,57 EUR

Pattern Identificati:

35 nuovi pattern specifici per Intesa Sanpaolo 3 nuovi campi nella tabella principale Supporto multi-formato CSV per diverse banche Riconoscimento avanzato unità immobiliari (scala B int 10) Estrazione automatica mittenti, fatture, polizze

Il sistema ora supporta UNICREDIT e INTESA SANPAOLO con riconoscimento automatico completo! 🎯