Files
Data-Coupler/PRE_DISCOVERY_SYSTEM.md
T
Alessio 39d7124ce1 feat: Implementato sistema Pre-Discovery con supporto esecuzioni schedulate
- Fix FindEntitiesByKeysAsync: SOQL query universale al posto di External ID GET
  * Disabilitato External ID GET (funziona solo per campi marcati External ID)
  * Query SOQL come metodo primario funzionante per tutti i campi
  * Logging dettagliato per diagnostica ricerche Salesforce

- Pre-Discovery nei trasferimenti manuali (DataCoupler.razor.cs)
  * Ricerca automatica nella destinazione quando KeyAssociation non esiste
  * Creazione associazione con marker CreatedBy="PreDiscovery"
  * Aggiornamento forzato senza controllo hash per associazioni Pre-Discovery
  * Supporto parallel processing thread-safe

- Pre-Discovery nei trasferimenti schedulati (ScheduledProfileExecutionService.cs)
  * Logica identica a trasferimenti manuali
  * Marker aggiuntivo ScheduledTransfer=true per tracciabilità
  * Aggiornamento forzato per prima sincronizzazione

- Sistema aggiornamento forzato
  * Verifica AdditionalInfo per identificare associazioni Pre-Discovery
  * Skip controllo hash per associazioni appena scoperte
  * Garantisce sincronizzazione dati al primo trasferimento

Vantaggi:
- Prevenzione automatica duplicati in Salesforce
- Recupero record esistenti senza associazioni
- Parità funzionale tra esecuzioni manuali e schedulate
- Performance ottimizzate con controllo hash per esecuzioni successive

Docs: PRE_DISCOVERY_SYSTEM.md, PRE_DISCOVERY_FORCED_UPDATE.md, SALESFORCE_FIND_ENTITIES_FIX.md
2025-10-21 00:48:03 +02:00

11 KiB

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

// 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:

// Prepara ricerca usando il campo mappato
var searchFields = new Dictionary<string, object>
{
    { 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:

// 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:

// 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

// 🔍 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<string, object>
            {
                { 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