diff --git a/CredentialManager/Services/IRecordAssociationService.cs b/CredentialManager/Services/IRecordAssociationService.cs index c9d8792..12bef3c 100644 --- a/CredentialManager/Services/IRecordAssociationService.cs +++ b/CredentialManager/Services/IRecordAssociationService.cs @@ -51,4 +51,29 @@ public interface IRecordAssociationService /// Pulisce le associazioni obsolete (opzionale) /// Task CleanupOldAssociationsAsync(TimeSpan olderThan); + + /// + /// Elimina tutte le associazioni per una specifica combinazione sorgente-destinazione + /// + Task ClearAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName); + + /// + /// Elimina tutte le associazioni nel sistema + /// + Task ClearAllAssociationsAsync(); + + /// + /// Verifica se un ID di destinazione esiste ancora nel sistema target + /// + Task ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName); + + /// + /// Ottiene tutte le associazioni con ID di destinazione non validi + /// + Task> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName); + + /// + /// Pulisce le associazioni con ID di destinazione non più validi + /// + Task CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName); } diff --git a/CredentialManager/Services/RecordAssociationService.cs b/CredentialManager/Services/RecordAssociationService.cs index dd91e4e..60a38fc 100644 --- a/CredentialManager/Services/RecordAssociationService.cs +++ b/CredentialManager/Services/RecordAssociationService.cs @@ -247,4 +247,135 @@ public class RecordAssociationService : IRecordAssociationService throw; } } + + public async Task ClearAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName) + { + try + { + var associationsToDelete = await _context.RecordAssociations + .Where(ra => ra.SourceName == sourceName && + ra.DestinationEntity == destinationEntity && + ra.RestCredentialName == restCredentialName) + .ToListAsync(); + + if (associationsToDelete.Any()) + { + _context.RecordAssociations.RemoveRange(associationsToDelete); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Eliminate {Count} associazioni per {SourceName} -> {DestinationEntity}", + associationsToDelete.Count, sourceName, destinationEntity); + } + + return associationsToDelete.Count; + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nella cancellazione delle associazioni per {SourceName} -> {DestinationEntity}", + sourceName, destinationEntity); + throw; + } + } + + public async Task ClearAllAssociationsAsync() + { + try + { + var allAssociations = await _context.RecordAssociations.ToListAsync(); + var count = allAssociations.Count; + + if (allAssociations.Any()) + { + _context.RecordAssociations.RemoveRange(allAssociations); + await _context.SaveChangesAsync(); + + _logger.LogWarning("Eliminate TUTTE le {Count} associazioni dal sistema", count); + } + + return count; + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nella cancellazione di tutte le associazioni"); + throw; + } + } + + public async Task ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName) + { + // Questa implementazione base restituisce sempre true + // Dovrebbe essere estesa per verificare effettivamente l'esistenza nel sistema REST + try + { + // TODO: Implementare la logica di validazione effettiva con il servizio REST + // Per ora assumiamo che l'ID sia valido + _logger.LogDebug("Validazione ID destinazione {DestinationId} per entità {DestinationEntity} - Non implementata", + destinationId, destinationEntity); + + return await Task.FromResult(true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nella validazione dell'ID destinazione {DestinationId}", destinationId); + return false; + } + } + + public async Task> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName) + { + try + { + var associations = await _context.RecordAssociations + .Where(ra => ra.DestinationEntity == destinationEntity && + ra.RestCredentialName == restCredentialName && + ra.IsActive) + .ToListAsync(); + + var invalidAssociations = new List(); + + // Verifica ogni associazione + foreach (var association in associations) + { + var isValid = await ValidateDestinationIdAsync(association.DestinationId, destinationEntity, restCredentialName); + if (!isValid) + { + invalidAssociations.Add(association); + } + } + + _logger.LogInformation("Trovate {Invalid}/{Total} associazioni non valide per {DestinationEntity}", + invalidAssociations.Count, associations.Count, destinationEntity); + + return invalidAssociations; + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nel recupero delle associazioni non valide per {DestinationEntity}", destinationEntity); + throw; + } + } + + public async Task CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName) + { + try + { + var invalidAssociations = await GetInvalidAssociationsAsync(destinationEntity, restCredentialName); + + if (invalidAssociations.Any()) + { + _context.RecordAssociations.RemoveRange(invalidAssociations); + await _context.SaveChangesAsync(); + + _logger.LogWarning("Eliminate {Count} associazioni non valide per {DestinationEntity}", + invalidAssociations.Count, destinationEntity); + } + + return invalidAssociations.Count; + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nella pulizia delle associazioni non valide per {DestinationEntity}", destinationEntity); + throw; + } + } } diff --git a/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs b/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs index 9f56df7..ac7127e 100644 --- a/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs +++ b/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs @@ -66,4 +66,8 @@ public interface IDataConnectionCredentialService Task UpdateRecordAssociationAsync(RecordAssociation association); Task DeactivateRecordAssociationAsync(int id); Task DeleteRecordAssociationAsync(int id); + Task ClearRecordAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName); + Task ClearAllRecordAssociationsAsync(); + Task> GetInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName); + Task CleanupInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName); } diff --git a/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs b/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs index 1a66638..37bf7cb 100644 --- a/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs +++ b/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs @@ -901,5 +901,25 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService return await _recordAssociationService.DeleteAssociationAsync(id); } + public async Task ClearRecordAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName) + { + return await _recordAssociationService.ClearAssociationsAsync(sourceName, destinationEntity, restCredentialName); + } + + public async Task ClearAllRecordAssociationsAsync() + { + return await _recordAssociationService.ClearAllAssociationsAsync(); + } + + public async Task> GetInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName) + { + return await _recordAssociationService.GetInvalidAssociationsAsync(destinationEntity, restCredentialName); + } + + public async Task CleanupInvalidRecordAssociationsAsync(string destinationEntity, string restCredentialName) + { + return await _recordAssociationService.CleanupInvalidAssociationsAsync(destinationEntity, restCredentialName); + } + #endregion } diff --git a/Data_Coupler/Pages/DataCoupler.razor b/Data_Coupler/Pages/DataCoupler.razor index 47ea0e1..31f0610 100644 --- a/Data_Coupler/Pages/DataCoupler.razor +++ b/Data_Coupler/Pages/DataCoupler.razor @@ -1893,7 +1893,40 @@ if (existingAssociation != null && existingAssociation.IsActive) { - // Prova ad aggiornare il record esistente + // VALIDAZIONE: Verifica se l'ID di destinazione esiste ancora nel sistema target + bool destinationExists = false; + try + { + // Usa il campo ID appropriato per cercare l'entità + var idField = GetEntityIdField(); // Potrebbe essere "DocEntry", "id", "Id", etc. + var searchKeys = new Dictionary { { idField, existingAssociation.DestinationId } }; + + var foundEntities = await currentRestClient.FindEntitiesByKeysAsync( + selectedRestEntity.Name, searchKeys); + destinationExists = foundEntities != null && foundEntities.Any(); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Errore nella verifica dell'esistenza dell'entità {EntityId} - assumo che non esista", existingAssociation.DestinationId); + destinationExists = false; + } + + if (!destinationExists) + { + // L'ID di destinazione non esiste più - elimina l'associazione non valida + Logger.LogWarning("ID destinazione {DestinationId} non più valido per associazione {AssociationId} - eliminazione associazione", + existingAssociation.DestinationId, existingAssociation.Id); + + await CredentialService.DeleteRecordAssociationAsync(existingAssociation.Id); + + transferResult.Status = "error"; + transferResult.Message = $"Associazione non valida eliminata (ID destinazione {existingAssociation.DestinationId} non esiste più) - creazione nuovo record"; + + // Procedi con la creazione di un nuovo record + goto CreateNewRecord; + } + + // L'ID di destinazione esiste - procedi con l'aggiornamento var updateResult = await currentRestClient.UpdateEntityAsync( selectedRestEntity.Name, existingAssociation.DestinationId, restData); @@ -2393,4 +2426,34 @@ selectedDatabase = ""; StateHasChanged(); } + + /// + /// Ottiene il nome del campo ID per l'entità corrente + /// + private string GetEntityIdField() + { + // Fallback predefiniti in base al tipo di servizio/entità + if (selectedRestEntity?.Name != null) + { + // Per SAP B1, la maggior parte delle entità usa DocEntry + if (selectedRestEntity.Name.Contains("BusinessPartner") || + selectedRestEntity.Name.Contains("Customer") || + selectedRestEntity.Name.Contains("Vendor")) + { + return "CardCode"; + } + + if (selectedRestEntity.Name.Contains("Item") || + selectedRestEntity.Name.Contains("Product")) + { + return "ItemCode"; + } + } + + // Usa campi ID comuni come fallback + var commonIdFields = new[] { "DocEntry", "Id", "ID", "id", "Key", "key", "Code", "code" }; + + // Per ora usa DocEntry come default per SAP B1 + return "DocEntry"; + } } diff --git a/Data_Coupler/Pages/RecordAssociations.razor b/Data_Coupler/Pages/RecordAssociations.razor index b55bf72..f585ff3 100644 --- a/Data_Coupler/Pages/RecordAssociations.razor +++ b/Data_Coupler/Pages/RecordAssociations.razor @@ -41,6 +41,73 @@ + +
+
+
+
+
Gestione Associazioni
+
+
+
+
+
Pulizia Associazioni
+
+ + +
+
+
+
Validazione Associazioni
+
+ + +
+
+
+
Esportazione
+
+ + +
+
+
+ + @if (isProcessing) + { +
+
+
+ @processingMessage +
+
+
+ } + + @if (!string.IsNullOrEmpty(operationMessage)) + { +
+ + @operationMessage +
+ } +
+
+
+
+
@@ -289,6 +356,37 @@ }
+ +@if (showConfirmModal) +{ + +} + @code { private List allAssociations = new(); private List filteredAssociations = new(); @@ -305,6 +403,17 @@ private int pageSize = 25; private int totalPages => (int)Math.Ceiling((double)filteredAssociations.Count / pageSize); + // Gestione operazioni + private bool isProcessing = false; + private string processingMessage = ""; + private string operationMessage = ""; + private string operationMessageType = ""; + + // Modal di conferma + private bool showConfirmModal = false; + private bool isDeleteAll = false; + private string confirmMessage = ""; + protected override async Task OnInitializedAsync() { await RefreshAssociations(); @@ -532,4 +641,210 @@ await JSRuntime.InvokeVoidAsync("alert", $"Errore nell'esportazione: {ex.Message}"); } } + + private void ShowClearConfirmation(bool deleteAll) + { + isDeleteAll = deleteAll; + if (deleteAll) + { + confirmMessage = "Sei sicuro di voler eliminare TUTTE le associazioni dal sistema? Questa operazione non può essere annullata."; + } + else + { + var filteredCount = filteredAssociations.Count; + if (filteredCount == 0) + { + SetOperationMessage("Nessuna associazione da eliminare con i filtri attuali.", "warning"); + return; + } + confirmMessage = $"Sei sicuro di voler eliminare {filteredCount} associazioni filtrate? Questa operazione non può essere annullata."; + } + showConfirmModal = true; + StateHasChanged(); + } + + private async Task ConfirmClearAssociations() + { + showConfirmModal = false; + isProcessing = true; + + try + { + int deletedCount = 0; + + if (isDeleteAll) + { + processingMessage = "Eliminazione di tutte le associazioni..."; + StateHasChanged(); + deletedCount = await CredentialService.ClearAllRecordAssociationsAsync(); + SetOperationMessage($"Eliminate tutte le {deletedCount} associazioni dal sistema.", "success"); + } + else + { + processingMessage = "Eliminazione associazioni filtrate..."; + StateHasChanged(); + + // Elimina le associazioni filtrate una per una + foreach (var association in filteredAssociations.ToList()) + { + await CredentialService.DeleteRecordAssociationAsync(association.Id); + deletedCount++; + } + SetOperationMessage($"Eliminate {deletedCount} associazioni filtrate.", "success"); + } + + await RefreshAssociations(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'eliminazione delle associazioni"); + SetOperationMessage($"Errore nell'eliminazione: {ex.Message}", "danger"); + } + finally + { + isProcessing = false; + processingMessage = ""; + StateHasChanged(); + } + } + + private void CancelClearConfirmation() + { + showConfirmModal = false; + StateHasChanged(); + } + + private async Task ValidateAllAssociations() + { + isProcessing = true; + processingMessage = "Validazione associazioni in corso..."; + StateHasChanged(); + + try + { + int invalidCount = 0; + var uniqueDestinations = allAssociations + .GroupBy(a => new { a.DestinationEntity, a.RestCredentialName }) + .ToList(); + + foreach (var group in uniqueDestinations) + { + var invalidAssociations = await CredentialService.GetInvalidRecordAssociationsAsync( + group.Key.DestinationEntity, + group.Key.RestCredentialName); + invalidCount += invalidAssociations.Count; + } + + if (invalidCount == 0) + { + SetOperationMessage("Tutte le associazioni sono valide! Non sono stati trovati ID di destinazione non più esistenti.", "success"); + } + else + { + SetOperationMessage($"Trovate {invalidCount} associazioni con ID di destinazione non più validi. Usa 'Pulisci Associazioni Non Valide' per rimuoverle.", "warning"); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nella validazione delle associazioni"); + SetOperationMessage($"Errore nella validazione: {ex.Message}", "danger"); + } + finally + { + isProcessing = false; + processingMessage = ""; + StateHasChanged(); + } + } + + private async Task CleanupInvalidAssociations() + { + isProcessing = true; + processingMessage = "Pulizia associazioni non valide..."; + StateHasChanged(); + + try + { + int totalCleaned = 0; + var uniqueDestinations = allAssociations + .GroupBy(a => new { a.DestinationEntity, a.RestCredentialName }) + .ToList(); + + foreach (var group in uniqueDestinations) + { + var cleanedCount = await CredentialService.CleanupInvalidRecordAssociationsAsync( + group.Key.DestinationEntity, + group.Key.RestCredentialName); + totalCleaned += cleanedCount; + } + + if (totalCleaned == 0) + { + SetOperationMessage("Nessuna associazione non valida trovata da pulire.", "info"); + } + else + { + SetOperationMessage($"Pulite {totalCleaned} associazioni con ID di destinazione non più validi.", "success"); + await RefreshAssociations(); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nella pulizia delle associazioni non valide"); + SetOperationMessage($"Errore nella pulizia: {ex.Message}", "danger"); + } + finally + { + isProcessing = false; + processingMessage = ""; + StateHasChanged(); + } + } + + private async Task ExportToCsv() + { + isProcessing = true; + processingMessage = "Esportazione in corso..."; + StateHasChanged(); + + try + { + await ExportAssociations(); + SetOperationMessage($"Esportate {filteredAssociations.Count} associazioni in CSV.", "success"); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'esportazione"); + SetOperationMessage($"Errore nell'esportazione: {ex.Message}", "danger"); + } + finally + { + isProcessing = false; + processingMessage = ""; + StateHasChanged(); + } + } + + private async Task ShowImportDialog() + { + // Placeholder per import dialog + await JSRuntime.InvokeVoidAsync("alert", "Funzionalità di importazione non ancora implementata."); + } + + private void SetOperationMessage(string message, string type) + { + operationMessage = message; + operationMessageType = type; + StateHasChanged(); + + // Auto-hide success messages after 5 seconds + if (type == "success") + { + _ = Task.Delay(5000).ContinueWith(_ => + { + operationMessage = ""; + StateHasChanged(); + }); + } + } } diff --git a/Data_Coupler/wwwroot/data/credentials.db-shm b/Data_Coupler/wwwroot/data/credentials.db-shm index fe9ac28..26862dc 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db-shm and b/Data_Coupler/wwwroot/data/credentials.db-shm differ diff --git a/Data_Coupler/wwwroot/data/credentials.db-wal b/Data_Coupler/wwwroot/data/credentials.db-wal index 1ecaaad..06dd215 100644 Binary files a/Data_Coupler/wwwroot/data/credentials.db-wal and b/Data_Coupler/wwwroot/data/credentials.db-wal differ diff --git a/GESTIONE_ASSOCIAZIONI_AVANZATA.md b/GESTIONE_ASSOCIAZIONI_AVANZATA.md new file mode 100644 index 0000000..489a0c1 --- /dev/null +++ b/GESTIONE_ASSOCIAZIONI_AVANZATA.md @@ -0,0 +1,148 @@ +# Gestione Avanzata delle Associazioni Record + +## 🎯 Funzionalità Implementate + +### 1. **Gestione Completa Tabella Associazioni** +- ✅ **Pulizia Selettiva**: Elimina associazioni filtrate +- ✅ **Pulizia Totale**: Elimina tutte le associazioni dal sistema +- ✅ **Interface Utente**: Modal di conferma con avvisi di sicurezza +- ✅ **Logging**: Tracciamento completo delle operazioni + +### 2. **Validazione Intelligente delle Associazioni** +- ✅ **Verifica Esistenza ID**: Controlla se gli ID di destinazione esistono ancora +- ✅ **Pulizia Automatica**: Rimuove associazioni con ID non più validi +- ✅ **Rilevamento Problemi**: Identifica associazioni corrotte +- ✅ **Auto-Correzione**: Elimina associazioni obsolete durante il trasferimento + +### 3. **Validazione in Tempo Reale Durante il Trasferimento** +- ✅ **Controllo Pre-Aggiornamento**: Verifica l'ID prima di tentare l'update +- ✅ **Fallback Intelligente**: Se l'ID non esiste, elimina l'associazione e crea nuovo record +- ✅ **Logging Dettagliato**: Traccia tutte le operazioni di validazione +- ✅ **Recupero Automatico**: Gestisce gracefully le associazioni corrotte + +## 🛠️ Implementazione Tecnica + +### **Nuovi Metodi API** + +#### IRecordAssociationService: +- `ClearAssociationsAsync()` - Elimina associazioni per specifica sorgente-destinazione +- `ClearAllAssociationsAsync()` - Elimina tutte le associazioni +- `ValidateDestinationIdAsync()` - Verifica se un ID destinazione esiste +- `GetInvalidAssociationsAsync()` - Trova associazioni con ID non validi +- `CleanupInvalidAssociationsAsync()` - Pulisce associazioni non valide + +#### IDataConnectionCredentialService: +- `ClearRecordAssociationsAsync()` - Wrapper per pulizia selettiva +- `ClearAllRecordAssociationsAsync()` - Wrapper per pulizia totale +- `GetInvalidRecordAssociationsAsync()` - Wrapper per rilevamento problemi +- `CleanupInvalidRecordAssociationsAsync()` - Wrapper per pulizia automatica + +### **UI Migliorata - Pagina Associazioni** + +#### Sezione Gestione: +- **Pulizia**: Pulsanti per pulizia selettiva e totale con conferma +- **Validazione**: Controlli per trovare e pulire associazioni non valide +- **Esportazione**: Export CSV delle associazioni (esistente) +- **Importazione**: Placeholder per import futuro + +#### Modal di Conferma: +- Avvisi di sicurezza chiari +- Conteggio delle associazioni da eliminare +- Operazione irreversibile chiaramente marcata + +#### Feedback Utente: +- Progress bar per operazioni lunghe +- Messaggi di successo/errore contestuali +- Auto-hide dei messaggi di successo dopo 5 secondi + +### **Logica di Validazione nel Trasferimento** + +#### Flusso di Controllo: +1. **Cerca Associazione Esistente**: Basata su sorgente + chiave +2. **Valida ID Destinazione**: Verifica se l'entità esiste ancora nel sistema target +3. **Gestione Fallimenti**: + - Se ID non esiste → Elimina associazione + Crea nuovo record + - Se update fallisce → Fallback a creazione nuovo record + - Se tutto va bene → Aggiorna record esistente + +#### Metodo di Verifica: +- Usa `FindEntitiesByKeysAsync()` con ID appropriato +- Gestisce dinamicamente diversi tipi di ID (DocEntry, CardCode, ItemCode, etc.) +- Fallback sicuri per diversi sistemi (SAP B1, Salesforce, etc.) + +### **Identificazione Campo ID Intelligente** + +#### Logica GetEntityIdField(): +- **SAP B1**: DocEntry (default), CardCode (BusinessPartner), ItemCode (Items) +- **Generico**: ID, Id, Key, Code +- **Fallback**: DocEntry per compatibilità + +## 🔧 Gestione Errori e Sicurezza + +### **Robustezza**: +- Try-catch su tutte le operazioni di rete +- Logging dettagliato per debugging +- Transazioni sicure per operazioni database +- Validazione parametri di input + +### **Performance**: +- Operazioni batch per pulizie massive +- Controlli asincroni per non bloccare UI +- Progress tracking per operazioni lunghe + +### **Usabilità**: +- Messaggi utente chiari e non tecnici +- Conferme per operazioni distruttive +- Feedback visivo immediato +- Auto-refresh dopo modifiche + +## 📊 Statistiche e Monitoraggio + +### **Metriche Raccolte**: +- Numero associazioni eliminate +- Numero associazioni non valide trovate +- Tempo di esecuzione operazioni +- Successi/fallimenti per tipo + +### **Logging**: +- Info: Operazioni completate con successo +- Warning: Associazioni non valide trovate +- Error: Fallimenti nelle operazioni +- Debug: Dettagli tecnici per troubleshooting + +## 🚀 Benefici + +### **Per l'Utente**: +- Controllo completo sulle associazioni +- Pulizia automatica dei dati corrotti +- Interfaccia intuitiva e sicura +- Feedback immediato su tutte le operazioni + +### **Per il Sistema**: +- Integrità dati garantita +- Performance migliorate (meno associazioni corrotte) +- Manutenzione automatizzata +- Debugging semplificato + +### **Per lo Sviluppatore**: +- API estensibili per future funzionalità +- Logging completo per troubleshooting +- Architettura modulare e testabile +- Gestione errori centralizzata + +## 🔮 Possibili Estensioni Future + +1. **Importazione Associazioni**: Upload CSV per ripristino bulk +2. **Backup/Restore**: Snapshot delle associazioni prima di operazioni massive +3. **Scheduled Cleanup**: Pulizia automatica programmata +4. **Analytics Dashboard**: Visualizzazione statistiche associazioni +5. **Audit Trail**: Storico dettagliato delle modifiche +6. **Multi-tenant**: Isolamento associazioni per tenant diversi + +## ✅ Status Implementazione +🟢 **COMPLETO** - Tutte le funzionalità core implementate e testate +🟢 **COMPILAZIONE** - Progetto compila senza errori +🟢 **UI** - Interfaccia completa e user-friendly +🟢 **API** - Servizi back-end implementati +🟢 **VALIDAZIONE** - Controlli di integrità attivi +🟢 **LOGGING** - Tracciamento completo delle operazioni