# Sistema Pre-Discovery per Associazioni Esistenti ## Data: 21 Ottobre 2025 ## 🎯 Obiettivo Implementato un sistema di **pre-discovery automatico** che, prima di creare nuovi record, verifica se esistono già record nella destinazione che corrispondono ai dati sorgente. Se trovati, crea automaticamente un'associazione e procede con l'aggiornamento invece della creazione. ## 📋 Funzionamento ### Flusso Operativo Quando l'utente clicca su "Avvia trasferimento dati", per ogni record sorgente il sistema esegue i seguenti controlli: ``` 1. Verifica Associazione Locale ↓ 2. Se NON esiste → PRE-DISCOVERY nella Destinazione ↓ 3a. Trovato → Crea Associazione + Aggiorna Record ↓ 3b. NON Trovato → Crea Nuovo Record + Associazione ``` ### Dettaglio Step #### Step 1: Verifica Associazione Locale ```csharp // Cerca nella tabella KeyAssociations var existingAssociation = await CredentialService.FindKeyAssociationByValueParallelAsync( sourceKey, // Es: "C00001" currentEntityName, // Es: "Account" currentCredentialName // Es: "Salesforce_Prod" ); ``` **Risultato possibile:** - ✅ **Trovata**: Procedi con aggiornamento del record esistente - ❌ **Non trovata**: Passa allo Step 2 #### Step 2: Pre-Discovery nella Destinazione Se non esiste associazione locale, cerca direttamente nella destinazione REST: ```csharp // Prepara ricerca usando il campo mappato var searchFields = new Dictionary { { mappedDestinationFieldName, sourceKey } // Es: { "cardcode__c", "C00001" } }; // Cerca nella REST API var existingEntities = await currentRestClient.FindEntitiesByKeysAsync( currentEntityName, // "Account" searchFields // { "cardcode__c": "C00001" } ); ``` **Risultato possibile:** - ✅ **Trovato record**: Vai allo Step 3a - ❌ **Non trovato**: Vai allo Step 3b #### Step 3a: Creazione Associazione + Aggiornamento Se il record esiste nella destinazione: ```csharp // 1. Crea associazione var newAssociation = new KeyAssociation { KeyValue = sourceKey, // "C00001" SourceKeyField = sourceKeyField, // "CardCode" DestinationKeyField = "Id", // Campo ID standard MappedDestinationField = mappedDestinationField,// "cardcode__c" DestinationEntity = currentEntityName, // "Account" DestinationId = destinationId, // "001xx000003DGb2AAG" RestCredentialName = currentCredentialName, // "Salesforce_Prod" CreatedAt = DateTime.UtcNow, LastVerifiedAt = DateTime.UtcNow, IsActive = true, Data_Hash = currentDataHash, // Hash dei dati mappati AdditionalInfo = JSON con metadata "PreDiscovery" }; // 2. Salva associazione var associationId = await CredentialService.SaveKeyAssociationParallelAsync(newAssociation); // 3. Procedi con AGGIORNAMENTO del record esistente ``` #### Step 3b: Creazione Normale Se il record NON esiste: ```csharp // 1. Crea nuovo record nella destinazione var result = await restClient.CreateEntityAsync(entityName, data); // 2. Crea associazione con il nuovo ID var association = new KeyAssociation { ... }; await CredentialService.SaveKeyAssociationParallelAsync(association); ``` ## 🔧 Implementazione Tecnica ### File Modificati 1. **`Data_Coupler/Pages/DataCoupler.razor.cs`** - Metodo `StartDataTransferWithComposite()` - linea ~2620 - Metodo `StartDataTransferOriginal()` - linea ~1290 ### Codice Chiave ```csharp // 🔍 PRE-DISCOVERY: Se non esiste associazione, cerca nella destinazione if (existingAssociation == null) { Logger.LogDebug("PRE-DISCOVERY: Nessuna associazione trovata per '{KeyValue}'. Cerco nella destinazione...", sourceKey); // Cerca il campo destinazione mappato al campo chiave sorgente if (fieldMappings.TryGetValue(sourceKeyField, out var mappedDestinationFieldName)) { try { // Prepara i campi di ricerca var searchFields = new Dictionary { { mappedDestinationFieldName, sourceKey } }; // Cerca nella destinazione REST var existingEntities = await currentRestClient.FindEntitiesByKeysAsync( currentEntityName, searchFields); if (existingEntities != null && existingEntities.Count > 0) { var foundEntity = existingEntities[0]; var destinationId = foundEntity.ContainsKey("Id") ? foundEntity["Id"]?.ToString() : foundEntity.ContainsKey("id") ? foundEntity["id"]?.ToString() : null; if (!string.IsNullOrEmpty(destinationId)) { // Crea associazione e usa per aggiornamento var newAssociation = new KeyAssociation { ... }; var associationId = await CredentialService.SaveKeyAssociationParallelAsync(newAssociation); existingAssociation = newAssociation; existingAssociation.Id = associationId; } } } catch (Exception discEx) { Logger.LogWarning(discEx, "PRE-DISCOVERY: Errore durante la ricerca nella destinazione per KeyValue: '{KeyValue}'", sourceKey); } } } ``` ## 📊 Esempio Pratico ### Scenario: SAP Business One → Salesforce **Record Sorgente:** ``` CardCode: "C00001" CardName: "Acme Corp" City: "Milano" ``` **Mappings Configurati:** ``` CardCode → cardcode__c CardName → Name City → BillingCity ``` **Campo Chiave Selezionato:** `CardCode` ### Caso 1: Prima Esecuzione (Record Non Esiste) ``` 1. Cerca associazione → ❌ Non trovata 2. PRE-DISCOVERY in Salesforce → ❌ Non trovato 3. CREA nuovo Account in Salesforce 4. Crea associazione: CardCode="C00001" → Id="001xx000003DGb2AAG" ``` **Risultato:** - Account creato con ID `001xx000003DGb2AAG` - Associazione salvata in KeyAssociations ### Caso 2: Seconda Esecuzione (Record Già Esiste) ``` 1. Cerca associazione → ✅ TROVATA! 2. AGGIORNA Account esistente (ID: 001xx000003DGb2AAG) ``` **Risultato:** - Account aggiornato - Nessun duplicato ### Caso 3: Record Esiste ma Senza Associazione **Situazione:** Il record è stato creato manualmente in Salesforce con `cardcode__c = "C00001"` ma non esiste associazione. ``` 1. Cerca associazione → ❌ Non trovata 2. PRE-DISCOVERY in Salesforce → ✅ TROVATO! (ID: 001xx000003DGb2AAG) 3. CREA associazione automatica 4. AGGIORNA Account esistente (ID: 001xx000003DGb2AAG) ``` **Risultato:** - Associazione creata automaticamente - Account aggiornato invece di creare duplicato - ✨ Sincronizzazione senza duplicati! ## 🎁 Vantaggi ### 1. **Prevenzione Duplicati Automatica** - Il sistema trova record esistenti anche se creati manualmente - Non richiede pre-caricamento delle associazioni ### 2. **Sincronizzazione Intelligente** - Aggiorna invece di creare se il record esiste - Mantiene la coerenza tra sistemi ### 3. **Recupero Dati Legacy** - Se hai già dati in Salesforce, il sistema li "scopre" e crea le associazioni automaticamente - Utile per migrazioni da sistemi esistenti ### 4. **Performance Ottimizzate** - Ricerca parallela thread-safe - Cache delle associazioni già trovate ### 5. **Logging Dettagliato** - Traccia completa del processo di discovery - Debug facilitato con log prefissati `PRE-DISCOVERY:` ## 📋 Log di Esempio ### Discovery Successful ``` [Debug] PRE-DISCOVERY: Nessuna associazione trovata per 'C00001'. Cerco nella destinazione... [Debug] PRE-DISCOVERY: Cerco in 'Account' dove cardcode__c = 'C00001' [Info] PRE-DISCOVERY: ✅ Trovato record esistente! KeyValue: 'C00001' -> DestinationId: '001xx000003DGb2AAG' [Info] PRE-DISCOVERY: Associazione creata con ID: 42 [Info] COMPOSITE PARALLEL: Record 1 marcato per aggiornamento (EntityId: 001xx000003DGb2AAG) ``` ### Discovery Not Found ``` [Debug] PRE-DISCOVERY: Nessuna associazione trovata per 'C00001'. Cerco nella destinazione... [Debug] PRE-DISCOVERY: Cerco in 'Account' dove cardcode__c = 'C00001' [Debug] PRE-DISCOVERY: Nessun record esistente trovato per KeyValue: 'C00001' [Debug] COMPOSITE PARALLEL: Record 1 marcato per creazione ``` ### Discovery Error ``` [Warning] PRE-DISCOVERY: Errore durante la ricerca nella destinazione per KeyValue: 'C00001' System.Net.Http.HttpRequestException: Response status code does not indicate success: 400 (Bad Request) [Debug] COMPOSITE PARALLEL: Record 1 marcato per creazione ``` ## ⚙️ Configurazione ### Requisiti 1. **Campo Chiave Sorgente**: Deve essere selezionato e mappato 2. **Sistema Associazioni**: Deve essere abilitato (`useRecordAssociations = true`) 3. **Mapping Valido**: Il campo chiave deve essere presente nei field mappings ### Quando Si Attiva La pre-discovery si attiva **automaticamente** quando: - ✅ Il sistema di associazioni è abilitato - ✅ Non esiste associazione per il valore chiave - ✅ Il campo chiave è mappato a un campo destinazione - ✅ Il client REST supporta `FindEntitiesByKeysAsync` ### Quando NON Si Attiva - ❌ Associazione già presente (non serve discovery) - ❌ Campo chiave non mappato - ❌ Sistema associazioni disabilitato - ❌ Errore durante la ricerca (fallback a creazione normale) ## 🧪 Testing ### Test Case 1: Record Già in Destinazione **Setup:** 1. Crea manualmente un Account in Salesforce con `cardcode__c = "TEST001"` 2. Configura mapping: `CardCode → cardcode__c` 3. Seleziona `CardCode` come campo chiave **Esecuzione:** - Trasferisci record sorgente con `CardCode = "TEST001"` **Risultato Atteso:** - ✅ Pre-discovery trova il record - ✅ Associazione creata automaticamente - ✅ Record aggiornato (non creato duplicato) ### Test Case 2: Record Non Esiste **Setup:** 1. Salesforce NON contiene Account con `cardcode__c = "NEW001"` **Esecuzione:** - Trasferisci record sorgente con `CardCode = "NEW001"` **Risultato Atteso:** - ❌ Pre-discovery non trova nulla - ✅ Nuovo record creato - ✅ Associazione creata con nuovo ID ### Test Case 3: Più Trasferimenti **Setup:** 1. Esegui trasferimento 1 volta (crea record) **Esecuzione:** - Esegui trasferimento 2° volta con stessi dati **Risultato Atteso:** - ✅ Associazione trovata al primo controllo - ✅ Record aggiornato senza pre-discovery - ✅ Nessun duplicato creato ## 🔒 Sicurezza ### Thread-Safety - Usa `SaveKeyAssociationParallelAsync` per gestire race conditions - Le ricerche REST sono stateless e thread-safe ### Gestione Errori - Errori in pre-discovery non bloccano il trasferimento - Fallback automatico a creazione normale - Logging warning per diagnosi ### Validazione Dati - Verifica presenza ID valido prima di creare associazione - Controlla compatibilità Entity e Credential - Supporta varianti case-sensitive di "Id" field ## 📚 Riferimenti ### Metodi Utilizzati - `FindKeyAssociationByValueParallelAsync()` - `FindEntitiesByKeysAsync()` - `SaveKeyAssociationParallelAsync()` - `UpdateEntityAsync()` ### Entità Coinvolte - `KeyAssociation` - Tabella associazioni - `fieldMappings` - Dictionary mapping sorgente→destinazione - `IRestServiceClient` - Interfaccia REST per ricerca ## ✅ Status: IMPLEMENTATO Il sistema è completamente funzionale e attivo in entrambi i metodi di trasferimento: - ✅ `StartDataTransferWithComposite()` (Salesforce Composite API) - ✅ `StartDataTransferOriginal()` (REST API standard) --- **Versione**: 1.0 **Data Implementazione**: 21 Ottobre 2025 **Autore**: Sistema Data Coupler